challenger

OAuth 2.0-based authentication service that validates user can receive messages at a certain address
Log | Files | Refs | Submodules | README | LICENSE

test_challenger_db.c (34798B)


      1 /*
      2   This file is part of Challenger
      3   (C) 2023 Taler Systems SA
      4 
      5   Challenger 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   Challenger 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   Challenger; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file challengerdb/test_challenger_db.c
     18  * @brief testcase for challenger postgres db plugin
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"
     22 #include <gnunet/gnunet_util_lib.h>
     23 #include <gnunet/gnunet_pq_lib.h>
     24 #include <taler/taler_util.h>
     25 #include "challenger_database_lib.h"
     26 #include "challenger-database/drop_tables.h"
     27 #include "challenger-database/create_tables.h"
     28 #include "challenger-database/preflight.h"
     29 #include "challenger-database/gc.h"
     30 #include "challenger-database/insert_client.h"
     31 #include "challenger-database/do_insert_validation.h"
     32 #include "challenger-database/do_challenge_address.h"
     33 #include "challenger-database/do_solve_challenge.h"
     34 #include "challenger-database/do_insert_token.h"
     35 #include "challenger-database/get_token.h"
     36 #include "challenger-database/get_validation_pkce.h"
     37 #include "challenger-database/update_validation.h"
     38 #include "challenger_util.h"
     39 #include "pg_helper.h"
     40 #include "challenger-database/delete_client.h"
     41 #include "challenger-database/do_insert_validation.h"
     42 
     43 
     44 #define FAILIF(cond)                            \
     45         do {                                          \
     46           if (! (cond)) { break;}                       \
     47           GNUNET_break (0);                           \
     48           goto drop;                                     \
     49         } while (0)
     50 
     51 /**
     52  * Secret of the client the tests register and act as.
     53  */
     54 #define CLIENT_SECRET "secret-token:test-secret"
     55 
     56 /**
     57  * Redirect URI of the client the tests register.
     58  */
     59 #define CLIENT_URI "http://client.example.com/"
     60 
     61 /**
     62  * Global return value for the test.  Initially -1, set to 0 upon
     63  * completion.   Other values indicate some kind of error.
     64  */
     65 static int result;
     66 
     67 /**
     68  * Handle to the database we are testing.
     69  */
     70 static struct CHALLENGERDB_PostgresContext *pg;
     71 
     72 /**
     73  * Serial ID of the client registered by #setup_client().
     74  */
     75 static uint64_t client_id;
     76 
     77 
     78 /**
     79  * Register the OAuth client all tests act as.
     80  *
     81  * @return #GNUNET_OK on success
     82  */
     83 static enum GNUNET_GenericReturnValue
     84 setup_client (void)
     85 {
     86   bool uri_taken = false;
     87 
     88   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     89       CHALLENGERDB_insert_client (pg,
     90                                   CLIENT_URI,
     91                                   CLIENT_SECRET,
     92                                   &client_id,
     93                                   &uri_taken))
     94   {
     95     GNUNET_break (0);
     96     return GNUNET_SYSERR;
     97   }
     98   return GNUNET_OK;
     99 }
    100 
    101 
    102 /**
    103  * Create a fresh validation for #client_id and have a TAN transmitted
    104  * for it, leaving the validation in the state a user reaches by posting
    105  * an address to ``/challenge`` and then walking away.
    106  *
    107  * @param[out] nonce set to the nonce identifying the new validation
    108  * @param[out] pin set to the TAN that was "transmitted"
    109  * @return #GNUNET_OK on success
    110  */
    111 static enum GNUNET_GenericReturnValue
    112 challenge_validation (struct CHALLENGER_ValidationNonceP *nonce,
    113                       uint32_t *pin)
    114 {
    115   struct GNUNET_TIME_Absolute expiration
    116     = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
    117   json_t *address;
    118   char *state = NULL;
    119   char *redirect_uri = NULL;
    120   struct GNUNET_TIME_Absolute last_tx_time;
    121   uint32_t auth_attempts_left;
    122   uint32_t pin_transmissions_left;
    123   bool pin_transmit;
    124   bool address_refused;
    125   bool solved;
    126   bool failed;
    127   enum GNUNET_DB_QueryStatus qs;
    128 
    129   GNUNET_CRYPTO_random_block (nonce,
    130                               sizeof (*nonce));
    131   qs = CHALLENGERDB_do_insert_validation (pg,
    132                                           client_id,
    133                                           CLIENT_SECRET,
    134                                           nonce,
    135                                           expiration,
    136                                           NULL);
    137   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    138   {
    139     GNUNET_break (0);
    140     return GNUNET_SYSERR;
    141   }
    142   address = json_pack ("{s:s}",
    143                        "filename",
    144                        "test-challenger-db.txt");
    145   GNUNET_assert (NULL != address);
    146   *pin = 424242;
    147   qs = CHALLENGERDB_do_challenge_address (pg,
    148                                           nonce,
    149                                           address,
    150                                           GNUNET_TIME_UNIT_ZERO,
    151                                           pin,
    152                                           &state,
    153                                           &last_tx_time,
    154                                           &auth_attempts_left,
    155                                           &pin_transmissions_left,
    156                                           &pin_transmit,
    157                                           &redirect_uri,
    158                                           &address_refused,
    159                                           &solved,
    160                                           &failed);
    161   json_decref (address);
    162   /* The TAN is only pending until its transmission is confirmed; this
    163      stands in for a successful AUTH_COMMAND run. */
    164   qs = CHALLENGERDB_do_challenge_address_confirm_pin (pg,
    165                                                       nonce,
    166                                                       &auth_attempts_left);
    167   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    168   {
    169     GNUNET_break (0);
    170     return GNUNET_SYSERR;
    171   }
    172   GNUNET_free (state);
    173   GNUNET_free (redirect_uri);
    174   if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) ||
    175        (! pin_transmit) ||
    176        address_refused ||
    177        solved ||
    178        failed)
    179   {
    180     GNUNET_break (0);
    181     return GNUNET_SYSERR;
    182   }
    183   return GNUNET_OK;
    184 }
    185 
    186 
    187 /**
    188  * Enter @a pin for the validation under @a nonce and check it was accepted.
    189  *
    190  * @param nonce validation to solve
    191  * @param pin TAN to enter
    192  * @return #GNUNET_OK on success
    193  */
    194 static enum GNUNET_GenericReturnValue
    195 solve_validation (const struct CHALLENGER_ValidationNonceP *nonce,
    196                   uint32_t pin)
    197 {
    198   char *state = NULL;
    199   char *redirect_uri = NULL;
    200   uint32_t addr_left;
    201   uint32_t auth_attempts_left;
    202   uint32_t pin_transmissions_left;
    203   bool solved;
    204   bool exhausted;
    205   bool no_challenge;
    206   bool failed;
    207   enum GNUNET_DB_QueryStatus qs;
    208 
    209   qs = CHALLENGERDB_do_solve_challenge (pg,
    210                                         nonce,
    211                                         pin,
    212                                         &solved,
    213                                         &exhausted,
    214                                         &no_challenge,
    215                                         &failed,
    216                                         &state,
    217                                         &addr_left,
    218                                         &auth_attempts_left,
    219                                         &pin_transmissions_left,
    220                                         &redirect_uri);
    221   GNUNET_free (state);
    222   GNUNET_free (redirect_uri);
    223   if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) ||
    224        (! solved) ||
    225        exhausted ||
    226        no_challenge ||
    227        failed)
    228   {
    229     GNUNET_break (0);
    230     return GNUNET_SYSERR;
    231   }
    232   return GNUNET_OK;
    233 }
    234 
    235 
    236 /**
    237  * Mint an access token for the validation under @a nonce.
    238  *
    239  * @param client client redeeming the authorization code
    240  * @param nonce validation to redeem
    241  * @param[out] token set to the (random) access token we tried to store
    242  * @return transaction status of CHALLENGERDB_do_insert_token()
    243  */
    244 static enum GNUNET_DB_QueryStatus
    245 insert_token (uint64_t client,
    246               const struct CHALLENGER_ValidationNonceP *nonce,
    247               struct CHALLENGER_AccessTokenP *token)
    248 {
    249   GNUNET_CRYPTO_random_block (token,
    250                               sizeof (*token));
    251   return CHALLENGERDB_do_insert_token (pg,
    252                                        nonce,
    253                                        client,
    254                                        token,
    255                                        GNUNET_TIME_UNIT_HOURS,
    256                                        GNUNET_TIME_UNIT_DAYS);
    257 }
    258 
    259 
    260 /**
    261  * Test that a validation for which the user never entered the correct
    262  * TAN cannot be redeemed for an access token, and is not even visible to
    263  * the ``/token`` lookup.  Without the ``auth_attempts_left < 0`` guard
    264  * anyone able to compute the authorization code could mint an
    265  * attestation for an address nobody ever proved control over.
    266  *
    267  * @return #GNUNET_OK on success
    268  */
    269 static enum GNUNET_GenericReturnValue
    270 test_unsolved_not_redeemable (void)
    271 {
    272   struct CHALLENGER_ValidationNonceP nonce;
    273   struct CHALLENGER_AccessTokenP token;
    274   uint32_t pin;
    275 
    276   if (GNUNET_OK !=
    277       challenge_validation (&nonce,
    278                             &pin))
    279     return GNUNET_SYSERR;
    280   {
    281     char *client_secret;
    282     json_t *address;
    283     char *client_scope;
    284     char *client_state;
    285     char *client_redirect_uri;
    286     char *code_challenge;
    287     uint32_t code_challenge_method;
    288 
    289     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    290         CHALLENGERDB_get_validation_pkce (pg,
    291                                           &nonce,
    292                                           client_id,
    293                                           &client_secret,
    294                                           &address,
    295                                           &client_scope,
    296                                           &client_state,
    297                                           &client_redirect_uri,
    298                                           &code_challenge,
    299                                           &code_challenge_method))
    300     {
    301       GNUNET_break (0);
    302       return GNUNET_SYSERR;
    303     }
    304   }
    305   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    306       insert_token (client_id,
    307                     &nonce,
    308                     &token))
    309   {
    310     GNUNET_break (0);
    311     return GNUNET_SYSERR;
    312   }
    313   return GNUNET_OK;
    314 }
    315 
    316 
    317 /**
    318  * Create a fresh validation for #client_id whose address was set by the
    319  * client and marked read-only, so the user cannot change it.
    320  *
    321  * @param[out] nonce set to the nonce identifying the new validation
    322  * @return #GNUNET_OK on success
    323  */
    324 static enum GNUNET_GenericReturnValue
    325 setup_read_only_validation (struct CHALLENGER_ValidationNonceP *nonce)
    326 {
    327   struct GNUNET_TIME_Absolute expiration
    328     = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
    329   json_t *address;
    330   enum GNUNET_DB_QueryStatus qs;
    331 
    332   GNUNET_CRYPTO_random_block (nonce,
    333                               sizeof (*nonce));
    334   address = json_pack ("{s:s, s:b}",
    335                        "filename",
    336                        "test-challenger-db.txt",
    337                        "read_only",
    338                        true);
    339   GNUNET_assert (NULL != address);
    340   qs = CHALLENGERDB_do_insert_validation (pg,
    341                                           client_id,
    342                                           CLIENT_SECRET,
    343                                           nonce,
    344                                           expiration,
    345                                           address);
    346   json_decref (address);
    347   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    348   {
    349     GNUNET_break (0);
    350     return GNUNET_SYSERR;
    351   }
    352   return GNUNET_OK;
    353 }
    354 
    355 
    356 /**
    357  * Post the read-only address of the validation under @a nonce to
    358  * ``/challenge`` again, as the user would to get a (new) TAN.
    359  *
    360  * @param nonce validation to use
    361  * @param confirm true to confirm the transmission of a new TAN, false to
    362  *        act as if the transmission helper failed
    363  * @param[out] pin set to the TAN that was transmitted, if any
    364  * @param[out] pin_transmit set to true if a TAN was to be transmitted
    365  * @param[out] failed set to true if the validation failed permanently
    366  * @return #GNUNET_OK on success
    367  */
    368 static enum GNUNET_GenericReturnValue
    369 challenge_read_only (const struct CHALLENGER_ValidationNonceP *nonce,
    370                      bool confirm,
    371                      uint32_t *pin,
    372                      bool *pin_transmit,
    373                      bool *failed)
    374 {
    375   json_t *address;
    376   char *state = NULL;
    377   char *redirect_uri = NULL;
    378   struct GNUNET_TIME_Absolute last_tx_time;
    379   uint32_t auth_attempts_left;
    380   uint32_t pin_transmissions_left;
    381   bool address_refused;
    382   bool solved;
    383   enum GNUNET_DB_QueryStatus qs;
    384 
    385   address = json_pack ("{s:s, s:b}",
    386                        "filename",
    387                        "test-challenger-db.txt",
    388                        "read_only",
    389                        true);
    390   GNUNET_assert (NULL != address);
    391   *pin = GNUNET_CRYPTO_random_u32 (100000000);
    392   qs = CHALLENGERDB_do_challenge_address (pg,
    393                                           nonce,
    394                                           address,
    395                                           GNUNET_TIME_UNIT_ZERO,
    396                                           pin,
    397                                           &state,
    398                                           &last_tx_time,
    399                                           &auth_attempts_left,
    400                                           &pin_transmissions_left,
    401                                           pin_transmit,
    402                                           &redirect_uri,
    403                                           &address_refused,
    404                                           &solved,
    405                                           failed);
    406   json_decref (address);
    407   GNUNET_free (state);
    408   GNUNET_free (redirect_uri);
    409   if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) ||
    410        solved)
    411   {
    412     GNUNET_break (0);
    413     return GNUNET_SYSERR;
    414   }
    415   if (confirm && *pin_transmit)
    416   {
    417     qs = CHALLENGERDB_do_challenge_address_confirm_pin (pg,
    418                                                         nonce,
    419                                                         &auth_attempts_left);
    420     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    421     {
    422       GNUNET_break (0);
    423       return GNUNET_SYSERR;
    424     }
    425   }
    426   return GNUNET_OK;
    427 }
    428 
    429 
    430 /**
    431  * Enter the wrong TAN @a pin for the validation under @a nonce.
    432  *
    433  * @param nonce validation to use
    434  * @param pin (wrong) TAN to enter
    435  * @param[out] failed set to true if the validation failed permanently
    436  * @return #GNUNET_OK on success
    437  */
    438 static enum GNUNET_GenericReturnValue
    439 guess_wrong (const struct CHALLENGER_ValidationNonceP *nonce,
    440              uint32_t pin,
    441              bool *failed)
    442 {
    443   char *state = NULL;
    444   char *redirect_uri = NULL;
    445   uint32_t addr_left;
    446   uint32_t auth_attempts_left;
    447   uint32_t pin_transmissions_left;
    448   bool solved;
    449   bool exhausted;
    450   bool no_challenge;
    451   enum GNUNET_DB_QueryStatus qs;
    452 
    453   qs = CHALLENGERDB_do_solve_challenge (pg,
    454                                         nonce,
    455                                         pin,
    456                                         &solved,
    457                                         &exhausted,
    458                                         &no_challenge,
    459                                         failed,
    460                                         &state,
    461                                         &addr_left,
    462                                         &auth_attempts_left,
    463                                         &pin_transmissions_left,
    464                                         &redirect_uri);
    465   GNUNET_free (state);
    466   GNUNET_free (redirect_uri);
    467   if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) ||
    468        solved)
    469   {
    470     GNUNET_break (0);
    471     return GNUNET_SYSERR;
    472   }
    473   return GNUNET_OK;
    474 }
    475 
    476 
    477 /**
    478  * Test that a validation with a read-only address fails once all TAN
    479  * transmissions and guesses are spent.  The user cannot change a
    480  * read-only address, so ``address_attempts_left`` (still at its initial
    481  * value) must not keep the validation alive; the guess that uses up the
    482  * last chance already reports the failure, and a failed validation stays
    483  * failed.
    484  *
    485  * @return #GNUNET_OK on success
    486  */
    487 static enum GNUNET_GenericReturnValue
    488 test_read_only_exhaustion_fails (void)
    489 {
    490   struct CHALLENGER_ValidationNonceP nonce;
    491   uint32_t pin;
    492   bool pin_transmit;
    493   bool failed;
    494 
    495   if (GNUNET_OK !=
    496       setup_read_only_validation (&nonce))
    497     return GNUNET_SYSERR;
    498   for (unsigned int tx = 0; tx < 3; tx++)
    499   {
    500     if (GNUNET_OK !=
    501         challenge_read_only (&nonce,
    502                              true,
    503                              &pin,
    504                              &pin_transmit,
    505                              &failed))
    506       return GNUNET_SYSERR;
    507     if ( (! pin_transmit) ||
    508          failed)
    509     {
    510       GNUNET_break (0);
    511       return GNUNET_SYSERR;
    512     }
    513     for (unsigned int guess = 0; guess < 3; guess++)
    514     {
    515       if (GNUNET_OK !=
    516           guess_wrong (&nonce,
    517                        (pin + 1) % 100000000,
    518                        &failed))
    519         return GNUNET_SYSERR;
    520       if (failed != ( (2 == tx) && (2 == guess) ))
    521       {
    522         GNUNET_break (0);
    523         return GNUNET_SYSERR;
    524       }
    525     }
    526   }
    527   /* failed is final, whatever the user tries next */
    528   if (GNUNET_OK !=
    529       guess_wrong (&nonce,
    530                    pin,
    531                    &failed))
    532     return GNUNET_SYSERR;
    533   if (! failed)
    534   {
    535     GNUNET_break (0);
    536     return GNUNET_SYSERR;
    537   }
    538   if (GNUNET_OK !=
    539       challenge_read_only (&nonce,
    540                            true,
    541                            &pin,
    542                            &pin_transmit,
    543                            &failed))
    544     return GNUNET_SYSERR;
    545   if ( (! failed) ||
    546        pin_transmit)
    547   {
    548     GNUNET_break (0);
    549     return GNUNET_SYSERR;
    550   }
    551   return GNUNET_OK;
    552 }
    553 
    554 
    555 /**
    556  * Test that a validation fails if the transmission helper fails on the
    557  * last TAN after the guesses on the previous TAN were spent: the user
    558  * then never got anything left to try, even though no ``/solve`` spent
    559  * the last chance.  Also checks that ``/authorize`` reports the failure.
    560  *
    561  * @return #GNUNET_OK on success
    562  */
    563 static enum GNUNET_GenericReturnValue
    564 test_failed_last_transmission_fails (void)
    565 {
    566   struct CHALLENGER_ValidationNonceP nonce;
    567   uint32_t pin;
    568   bool pin_transmit;
    569   bool failed;
    570 
    571   if (GNUNET_OK !=
    572       setup_read_only_validation (&nonce))
    573     return GNUNET_SYSERR;
    574   if (GNUNET_OK !=
    575       challenge_read_only (&nonce,
    576                            true,
    577                            &pin,
    578                            &pin_transmit,
    579                            &failed))
    580     return GNUNET_SYSERR;
    581   for (unsigned int guess = 0; guess < 3; guess++)
    582     if (GNUNET_OK !=
    583         guess_wrong (&nonce,
    584                      (pin + 1) % 100000000,
    585                      &failed))
    586       return GNUNET_SYSERR;
    587   if (failed)
    588   {
    589     GNUNET_break (0);
    590     return GNUNET_SYSERR;
    591   }
    592   /* The remaining two transmissions fail. */
    593   for (unsigned int tx = 0; tx < 2; tx++)
    594   {
    595     if (GNUNET_OK !=
    596         challenge_read_only (&nonce,
    597                              false,
    598                              &pin,
    599                              &pin_transmit,
    600                              &failed))
    601       return GNUNET_SYSERR;
    602     if ( (! pin_transmit) ||
    603          failed)
    604     {
    605       GNUNET_break (0);
    606       return GNUNET_SYSERR;
    607     }
    608   }
    609   {
    610     json_t *last_address;
    611     uint32_t address_attempts_left;
    612     uint32_t pin_transmissions_left;
    613     uint32_t auth_attempts_left;
    614     bool solved;
    615     char *redirect_uri;
    616     struct GNUNET_TIME_Absolute last_tx_time;
    617 
    618     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    619         CHALLENGERDB_update_validation (pg,
    620                                         &nonce,
    621                                         client_id,
    622                                         NULL,
    623                                         "the-state",
    624                                         NULL,
    625                                         NULL,
    626                                         0,
    627                                         &last_address,
    628                                         &address_attempts_left,
    629                                         &pin_transmissions_left,
    630                                         &auth_attempts_left,
    631                                         &solved,
    632                                         &failed,
    633                                         &redirect_uri,
    634                                         &last_tx_time))
    635     {
    636       GNUNET_break (0);
    637       return GNUNET_SYSERR;
    638     }
    639     json_decref (last_address);
    640     if ( (! failed) ||
    641          (NULL == redirect_uri) ||
    642          (0 != strcmp (redirect_uri,
    643                        CLIENT_URI)) )
    644     {
    645       GNUNET_break (0);
    646       GNUNET_free (redirect_uri);
    647       return GNUNET_SYSERR;
    648     }
    649     GNUNET_free (redirect_uri);
    650   }
    651   if (GNUNET_OK !=
    652       guess_wrong (&nonce,
    653                    pin,
    654                    &failed))
    655     return GNUNET_SYSERR;
    656   if (! failed)
    657   {
    658     GNUNET_break (0);
    659     return GNUNET_SYSERR;
    660   }
    661   return GNUNET_OK;
    662 }
    663 
    664 
    665 /**
    666  * Test that a solved validation is redeemable, and redeemable exactly
    667  * once (the authorization code must not be replayable).
    668  *
    669  * @return #GNUNET_OK on success
    670  */
    671 static enum GNUNET_GenericReturnValue
    672 test_solved_redeemable (void)
    673 {
    674   struct CHALLENGER_ValidationNonceP nonce;
    675   struct CHALLENGER_AccessTokenP token;
    676   uint32_t pin;
    677 
    678   if ( (GNUNET_OK !=
    679         challenge_validation (&nonce,
    680                               &pin)) ||
    681        (GNUNET_OK !=
    682         solve_validation (&nonce,
    683                           pin)) )
    684     return GNUNET_SYSERR;
    685   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    686       insert_token (client_id,
    687                     &nonce,
    688                     &token))
    689   {
    690     GNUNET_break (0);
    691     return GNUNET_SYSERR;
    692   }
    693   /* replay: the validation was consumed, so this must not mint a token */
    694   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    695       insert_token (client_id,
    696                     &nonce,
    697                     &token))
    698   {
    699     GNUNET_break (0);
    700     return GNUNET_SYSERR;
    701   }
    702   return GNUNET_OK;
    703 }
    704 
    705 
    706 /**
    707  * Test that the address expiration stored with a freshly minted token is
    708  * in the future, and in particular that it does *not* depend on the
    709  * validation's ``last_tx_time``.  The regression this guards against
    710  * computed the expiration as ``address_expiration + last_tx_time``, which
    711  * lands in 1970 whenever that column was never written.  We force the
    712  * column back to 0 to pin the property down without relying on any
    713  * particular code path leaving it there.
    714  *
    715  * @return #GNUNET_OK on success
    716  */
    717 static enum GNUNET_GenericReturnValue
    718 test_address_expiry_is_in_the_future (void)
    719 {
    720   struct GNUNET_PQ_ExecuteStatement es[] = {
    721     GNUNET_PQ_make_execute ("UPDATE validations SET last_tx_time=0;"),
    722     GNUNET_PQ_EXECUTE_STATEMENT_END
    723   };
    724   struct CHALLENGER_ValidationNonceP nonce;
    725   struct CHALLENGER_AccessTokenP token;
    726   struct GNUNET_TIME_Timestamp address_expiration;
    727   struct GNUNET_TIME_Absolute now;
    728   json_t *address = NULL;
    729   uint64_t rowid;
    730   uint32_t pin;
    731   enum GNUNET_GenericReturnValue ret;
    732 
    733   if ( (GNUNET_OK !=
    734         challenge_validation (&nonce,
    735                               &pin)) ||
    736        (GNUNET_OK !=
    737         solve_validation (&nonce,
    738                           pin)) )
    739     return GNUNET_SYSERR;
    740   if (GNUNET_OK !=
    741       GNUNET_PQ_exec_statements (pg->conn,
    742                                  es))
    743   {
    744     GNUNET_break (0);
    745     return GNUNET_SYSERR;
    746   }
    747   now = GNUNET_TIME_absolute_get ();
    748   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    749       insert_token (client_id,
    750                     &nonce,
    751                     &token))
    752   {
    753     GNUNET_break (0);
    754     return GNUNET_SYSERR;
    755   }
    756   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    757       CHALLENGERDB_get_token (pg,
    758                               &token,
    759                               &rowid,
    760                               &address,
    761                               &address_expiration))
    762   {
    763     GNUNET_break (0);
    764     return GNUNET_SYSERR;
    765   }
    766   ret = GNUNET_OK;
    767   if (address_expiration.abs_time.abs_value_us <= now.abs_value_us)
    768   {
    769     GNUNET_break (0);
    770     ret = GNUNET_SYSERR;
    771   }
    772   json_decref (address);
    773   return ret;
    774 }
    775 
    776 
    777 /**
    778  * Test that an expired validation is not consumable, even though the
    779  * caller (``/token``) checked the expiration in an earlier, separate
    780  * transaction.  Everything get_validation_pkce() filters on has to be
    781  * repeated by do_insert_token(), or the gap between the two statements is
    782  * an unguarded TOCTOU window.
    783  *
    784  * @return #GNUNET_OK on success
    785  */
    786 static enum GNUNET_GenericReturnValue
    787 test_expired_not_redeemable (void)
    788 {
    789   struct GNUNET_PQ_ExecuteStatement es[] = {
    790     GNUNET_PQ_make_execute ("UPDATE validations SET expiration_time=1;"),
    791     GNUNET_PQ_EXECUTE_STATEMENT_END
    792   };
    793   struct CHALLENGER_ValidationNonceP nonce;
    794   struct CHALLENGER_AccessTokenP token;
    795   uint32_t pin;
    796 
    797   if ( (GNUNET_OK !=
    798         challenge_validation (&nonce,
    799                               &pin)) ||
    800        (GNUNET_OK !=
    801         solve_validation (&nonce,
    802                           pin)) )
    803     return GNUNET_SYSERR;
    804   if (GNUNET_OK !=
    805       GNUNET_PQ_exec_statements (pg->conn,
    806                                  es))
    807   {
    808     GNUNET_break (0);
    809     return GNUNET_SYSERR;
    810   }
    811   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    812       insert_token (client_id,
    813                     &nonce,
    814                     &token))
    815   {
    816     GNUNET_break (0);
    817     return GNUNET_SYSERR;
    818   }
    819   return GNUNET_OK;
    820 }
    821 
    822 
    823 /**
    824  * Test that a validation cannot be consumed by a client it does not
    825  * belong to.
    826  *
    827  * @return #GNUNET_OK on success
    828  */
    829 static enum GNUNET_GenericReturnValue
    830 test_foreign_client_not_redeemable (void)
    831 {
    832   struct CHALLENGER_ValidationNonceP nonce;
    833   struct CHALLENGER_AccessTokenP token;
    834   uint32_t pin;
    835 
    836   if ( (GNUNET_OK !=
    837         challenge_validation (&nonce,
    838                               &pin)) ||
    839        (GNUNET_OK !=
    840         solve_validation (&nonce,
    841                           pin)) )
    842     return GNUNET_SYSERR;
    843   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    844       insert_token (client_id + 1,
    845                     &nonce,
    846                     &token))
    847   {
    848     GNUNET_break (0);
    849     return GNUNET_SYSERR;
    850   }
    851   /* ... and the rightful client still can */
    852   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    853       insert_token (client_id,
    854                     &nonce,
    855                     &token))
    856   {
    857     GNUNET_break (0);
    858     return GNUNET_SYSERR;
    859   }
    860   return GNUNET_OK;
    861 }
    862 
    863 
    864 /**
    865  * Run @a sql directly on the database, bypassing the CHALLENGERDB API.
    866  * Used to create states (and to make assertions about the schema) that
    867  * the API deliberately cannot produce.
    868  *
    869  * @param sql SQL statement to execute
    870  * @return #GNUNET_OK on success
    871  */
    872 static enum GNUNET_GenericReturnValue
    873 exec_sql (const char *sql)
    874 {
    875   struct GNUNET_PQ_ExecuteStatement es[] = {
    876     GNUNET_PQ_make_execute (sql),
    877     GNUNET_PQ_EXECUTE_STATEMENT_END
    878   };
    879 
    880   return GNUNET_PQ_exec_statements (pg->conn,
    881                                     es);
    882 }
    883 
    884 
    885 /**
    886  * Test that a client cannot be inserted silently: a URI that is already
    887  * registered must be reported as such, and any *other* unique violation
    888  * (notably one on the primary key, which happens once the identity sequence
    889  * of 'clients' lags behind max(client_serial_id)) must not be misreported as
    890  * a duplicate URI.
    891  *
    892  * @return #GNUNET_OK on success
    893  */
    894 static enum GNUNET_GenericReturnValue
    895 test_insert_client (void)
    896 {
    897   uint64_t nclient_id;
    898   bool uri_taken;
    899 
    900   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    901       CHALLENGERDB_insert_client (pg,
    902                                   "https://example.com/a",
    903                                   "secret-token:a",
    904                                   &nclient_id,
    905                                   &uri_taken))
    906   {
    907     GNUNET_break (0);
    908     return GNUNET_SYSERR;
    909   }
    910   if (uri_taken)
    911   {
    912     GNUNET_break (0);
    913     return GNUNET_SYSERR;
    914   }
    915   /* Same URI again: this one really is a duplicate. */
    916   if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    917         CHALLENGERDB_insert_client (pg,
    918                                     "https://example.com/a",
    919                                     "secret-token:b",
    920                                     &nclient_id,
    921                                     &uri_taken)) ||
    922        (! uri_taken) )
    923   {
    924     GNUNET_break (0);
    925     return GNUNET_SYSERR;
    926   }
    927   /* Rewind the identity sequence, as a restore that did not restore the
    928      sequences leaves it: the next INSERT then collides on the primary key,
    929      not on the URI. */
    930   if (GNUNET_OK !=
    931       exec_sql ("DO $$ BEGIN"
    932                 " PERFORM setval("
    933                 "   pg_get_serial_sequence('challenger.clients',"
    934                 "                          'client_serial_id'),"
    935                 "   (SELECT MIN(client_serial_id) FROM challenger.clients),"
    936                 "   false);"
    937                 "END $$;"))
    938   {
    939     GNUNET_break (0);
    940     return GNUNET_SYSERR;
    941   }
    942   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
    943       CHALLENGERDB_insert_client (pg,
    944                                   "https://example.com/b",
    945                                   "secret-token:b",
    946                                   &nclient_id,
    947                                   &uri_taken))
    948   {
    949     /* The PK collision must not go unnoticed. */
    950     GNUNET_break (0);
    951     return GNUNET_SYSERR;
    952   }
    953   if (uri_taken)
    954   {
    955     /* This is the regression: the failure has nothing to do with the URI,
    956        and the operator must not be told that the URI already exists. */
    957     GNUNET_break (0);
    958     return GNUNET_SYSERR;
    959   }
    960   /* Repair the sequence, then the very same insert must work. */
    961   if (GNUNET_OK !=
    962       exec_sql ("DO $$ BEGIN"
    963                 " PERFORM setval("
    964                 "   pg_get_serial_sequence('challenger.clients',"
    965                 "                          'client_serial_id'),"
    966                 "   (SELECT MAX(client_serial_id) FROM challenger.clients),"
    967                 "   true);"
    968                 "END $$;"))
    969   {
    970     GNUNET_break (0);
    971     return GNUNET_SYSERR;
    972   }
    973   if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    974         CHALLENGERDB_insert_client (pg,
    975                                     "https://example.com/b",
    976                                     "secret-token:b",
    977                                     &nclient_id,
    978                                     &uri_taken)) ||
    979        (uri_taken) )
    980   {
    981     GNUNET_break (0);
    982     return GNUNET_SYSERR;
    983   }
    984   return GNUNET_OK;
    985 }
    986 
    987 
    988 /**
    989  * Test that deleting a client reports how many validations it took with it:
    990  * validations_client_serial_id_fkey cascades, so an operator would otherwise
    991  * abort in-flight KYC processes without being told.
    992  *
    993  * @return #GNUNET_OK on success
    994  */
    995 static enum GNUNET_GenericReturnValue
    996 test_delete_client (void)
    997 {
    998   const char *uri = "https://example.com/del";
    999   const char *secret = "secret-token:del";
   1000   struct GNUNET_TIME_Absolute expiration
   1001     = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
   1002   uint64_t nclient_id;
   1003   uint64_t validations_deleted;
   1004   bool uri_taken;
   1005 
   1006   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
   1007       CHALLENGERDB_insert_client (pg,
   1008                                   uri,
   1009                                   secret,
   1010                                   &nclient_id,
   1011                                   &uri_taken))
   1012   {
   1013     GNUNET_break (0);
   1014     return GNUNET_SYSERR;
   1015   }
   1016   for (unsigned int i = 0; i < 2; i++)
   1017   {
   1018     struct CHALLENGER_ValidationNonceP nonce;
   1019 
   1020     GNUNET_CRYPTO_random_block (&nonce,
   1021                                 sizeof (nonce));
   1022     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
   1023         CHALLENGERDB_do_insert_validation (pg,
   1024                                            nclient_id,
   1025                                            secret,
   1026                                            &nonce,
   1027                                            expiration,
   1028                                            NULL))
   1029     {
   1030       GNUNET_break (0);
   1031       return GNUNET_SYSERR;
   1032     }
   1033   }
   1034   if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
   1035         CHALLENGERDB_delete_client (pg,
   1036                                     uri,
   1037                                     &validations_deleted)) ||
   1038        (2 != validations_deleted) )
   1039   {
   1040     GNUNET_break (0);
   1041     return GNUNET_SYSERR;
   1042   }
   1043   /* Deleting it again finds nothing and discards nothing. */
   1044   if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
   1045         CHALLENGERDB_delete_client (pg,
   1046                                     uri,
   1047                                     &validations_deleted)) ||
   1048        (0 != validations_deleted) )
   1049   {
   1050     GNUNET_break (0);
   1051     return GNUNET_SYSERR;
   1052   }
   1053   return GNUNET_OK;
   1054 }
   1055 
   1056 
   1057 /**
   1058  * Main function that will be run by the scheduler.
   1059  *
   1060  * @param cls closure with config
   1061  */
   1062 static void
   1063 run (void *cls)
   1064 {
   1065   struct GNUNET_CONFIGURATION_Handle *cfg = cls;
   1066 
   1067   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
   1068               "Connecting\n");
   1069   if (NULL == (pg = CHALLENGERDB_connect_admin (cfg)))
   1070   {
   1071     result = 77;
   1072     return;
   1073   }
   1074   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
   1075               "Connected\n");
   1076   if (GNUNET_OK !=
   1077       CHALLENGERDB_drop_tables (pg))
   1078   {
   1079     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
   1080                 "Dropping tables failed\n");
   1081   }
   1082   if (GNUNET_OK !=
   1083       CHALLENGERDB_create_tables (pg))
   1084   {
   1085     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1086                 "Creating tables failed\n");
   1087     goto drop;
   1088   }
   1089   GNUNET_assert (GNUNET_OK ==
   1090                  CHALLENGERDB_preflight (pg));
   1091   {
   1092     struct GNUNET_TIME_Absolute ts = GNUNET_TIME_absolute_get ();
   1093 
   1094     FAILIF (0 >
   1095             CHALLENGERDB_gc (pg,
   1096                              ts));
   1097   }
   1098   FAILIF (GNUNET_OK !=
   1099           setup_client ());
   1100   FAILIF (GNUNET_OK !=
   1101           test_unsolved_not_redeemable ());
   1102   FAILIF (GNUNET_OK !=
   1103           test_solved_redeemable ());
   1104   FAILIF (GNUNET_OK !=
   1105           test_address_expiry_is_in_the_future ());
   1106   FAILIF (GNUNET_OK !=
   1107           test_expired_not_redeemable ());
   1108   FAILIF (GNUNET_OK !=
   1109           test_foreign_client_not_redeemable ());
   1110   FAILIF (GNUNET_OK !=
   1111           test_read_only_exhaustion_fails ());
   1112   FAILIF (GNUNET_OK !=
   1113           test_failed_last_transmission_fails ());
   1114   FAILIF (GNUNET_OK !=
   1115           test_insert_client ());
   1116   FAILIF (GNUNET_OK !=
   1117           test_delete_client ());
   1118   result = 0;
   1119 drop:
   1120   GNUNET_break (GNUNET_OK ==
   1121                 CHALLENGERDB_drop_tables (pg));
   1122   CHALLENGERDB_disconnect (pg);
   1123   pg = NULL;
   1124 }
   1125 
   1126 
   1127 int
   1128 main (int argc,
   1129       char *const argv[])
   1130 {
   1131   struct GNUNET_CONFIGURATION_Handle *cfg;
   1132 
   1133   (void) argc;
   1134   result = EXIT_FAILURE;
   1135   GNUNET_log_setup (argv[0],
   1136                     "DEBUG",
   1137                     NULL);
   1138   cfg = GNUNET_CONFIGURATION_create (CHALLENGER_project_data ());
   1139   if (GNUNET_OK !=
   1140       GNUNET_CONFIGURATION_parse (cfg,
   1141                                   "test_challenger_db_postgres.conf"))
   1142   {
   1143     GNUNET_break (0);
   1144     return EXIT_NOTCONFIGURED;
   1145   }
   1146   GNUNET_SCHEDULER_run (&run, cfg);
   1147   GNUNET_CONFIGURATION_destroy (cfg);
   1148   return result;
   1149 }
   1150 
   1151 
   1152 /* end of test_challenger_db.c */