anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

do_verify_challenge_code.c (8419B)


      1 /*
      2   This file is part of Anastasis
      3   Copyright (C) 2020-2022 Anastasis SARL
      4 
      5   Anastasis is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   Anastasis 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 Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   Anastasis; see the file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file stasis/do_verify_challenge_code.c
     18  * @brief Anastasis database: do verify challenge code
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"
     22 #include "anastasis-db_pg.h"
     23 #include "anastasis/anastasis-database/do_verify_challenge_code.h"
     24 #include "anastasis/anastasis-database/transaction.h"
     25 #include "anastasis/anastasis-database/preflight.h"
     26 #include <taler/taler_pq_lib.h>
     27 
     28 
     29 struct CheckValidityContext
     30 {
     31   /**
     32    * Code to check for.
     33    */
     34   const struct GNUNET_HashCode *hashed_code;
     35 
     36   /**
     37    * Truth we are processing.
     38    */
     39   const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid;
     40 
     41   /**
     42    * Set to the matching challenge code (if @e valid).
     43    */
     44   uint64_t code;
     45 
     46   /**
     47    * Set to true if a code matching @e hashed_code was found.
     48    */
     49   bool valid;
     50 
     51   /**
     52    * Set to true if a code matching @e hashed_code was set to 'satisfied' by the plugin.
     53    */
     54   bool satisfied;
     55 
     56   /**
     57    * Set to true if the answerable code is out of attempts.
     58    */
     59   bool exhausted;
     60 
     61   /**
     62    * Set to true if we had a database failure.
     63    */
     64   bool db_failure;
     65 
     66 };
     67 
     68 
     69 /**
     70  * Helper function for #postgres_verify_challenge_code().
     71  * To be called with the results of a SELECT statement
     72  * that has returned @a num_results results.
     73  *
     74  * @param cls closure of type `struct CheckValidityContext *`
     75  * @param result the postgres result
     76  * @param num_results the number of results in @a result
     77  */
     78 static void
     79 check_valid_code (void *cls,
     80                   PGresult *result,
     81                   unsigned int num_results)
     82 {
     83   struct CheckValidityContext *cvc = cls;
     84 
     85   for (unsigned int i = 0; i < num_results; i++)
     86   {
     87     uint64_t server_code;
     88     uint32_t retry_counter;
     89     uint8_t sat;
     90     struct GNUNET_PQ_ResultSpec rs[] = {
     91       GNUNET_PQ_result_spec_uint64 ("code",
     92                                     &server_code),
     93       GNUNET_PQ_result_spec_uint32 ("retry_counter",
     94                                     &retry_counter),
     95       GNUNET_PQ_result_spec_auto_from_type ("satisfied",
     96                                             &sat),
     97       GNUNET_PQ_result_spec_end
     98     };
     99 
    100     if (GNUNET_OK !=
    101         GNUNET_PQ_extract_result (result,
    102                                   rs,
    103                                   i))
    104     {
    105       GNUNET_break (0);
    106       cvc->db_failure = true;
    107       return;
    108     }
    109     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    110                 "Found issued challenge %llu (client: %s)\n",
    111                 (unsigned long long) server_code,
    112                 GNUNET_h2s (cvc->hashed_code));
    113     if (0 == retry_counter)
    114     {
    115       /* The answerable code is out of attempts.  Do not fall back to an
    116          older sibling that still has attempts left: that would make the
    117          effective budget 3 times the number of live codes.  Do not answer
    118          it either, not even correctly -- the counter is the rate limit. */
    119       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    120                   "Challenge %llu is out of attempts, rate limiting\n",
    121                   (unsigned long long) server_code);
    122       cvc->exhausted = true;
    123       return;
    124     }
    125     {
    126       struct GNUNET_HashCode shashed_code;
    127 
    128       ANASTASIS_hash_answer (server_code,
    129                              &shashed_code);
    130       if (0 ==
    131           GNUNET_memcmp (&shashed_code,
    132                          cvc->hashed_code))
    133       {
    134         GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    135                     "Challenge is valid challenge (%s)\n",
    136                     (0 != sat) ? "satisfied" : "not satisfied");
    137         cvc->valid = true;
    138         cvc->code = server_code;
    139         cvc->satisfied = (0 != sat);
    140       }
    141       else
    142       {
    143         /* count failures to prevent brute-force attacks */
    144         struct GNUNET_PQ_QueryParam params[] = {
    145           GNUNET_PQ_query_param_auto_from_type (cvc->truth_uuid),
    146           GNUNET_PQ_query_param_uint64 (&server_code),
    147           GNUNET_PQ_query_param_end
    148         };
    149         enum GNUNET_DB_QueryStatus qs;
    150 
    151         qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
    152                                                  "do_verify_challenge_code_update_retry_counter",
    153                                                  params);
    154         if (qs <= 0)
    155         {
    156           GNUNET_break (0);
    157           cvc->db_failure = true;
    158         }
    159       }
    160     }
    161   }
    162 }
    163 
    164 
    165 /**
    166  * Verify the provided code with the code on the server.
    167  * Only the newest unexpired code issued for @a truth_uuid is answerable.
    168  * If the code matches the function will return with success, if the code
    169  * does not match, the retry counter will be decreased by one.  Once that
    170  * counter reaches zero the code is no longer answerable and
    171  * #ANASTASIS_DB_CODE_STATUS_RATE_LIMITED is returned until the next code
    172  * is issued.
    173  *
    174  * @param truth_uuid identification of the challenge which the code corresponds to
    175  * @param hashed_code code which the user provided and wants to verify
    176  * @param[out] code set to the original numeric code
    177  * @param[out] satisfied set to true if the challenge is set to satisfied
    178  * @return code validity status
    179  */
    180 enum ANASTASIS_DB_CodeStatus
    181 ANASTASIS_DB_do_verify_challenge_code (
    182   const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
    183   const struct GNUNET_HashCode *hashed_code,
    184   uint64_t *code,
    185   bool *satisfied)
    186 {
    187   struct CheckValidityContext cvc = {
    188     .truth_uuid = truth_uuid,
    189     .hashed_code = hashed_code,
    190   };
    191   struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
    192   struct GNUNET_PQ_QueryParam params[] = {
    193     GNUNET_PQ_query_param_auto_from_type (truth_uuid),
    194     GNUNET_PQ_query_param_timestamp (&now),
    195     GNUNET_PQ_query_param_end
    196   };
    197   enum GNUNET_DB_QueryStatus qs;
    198 
    199   *satisfied = false;
    200   PREPARE ("do_verify_challenge_code_update_retry_counter",
    201            "UPDATE anastasis_challengecode"
    202            " SET retry_counter=retry_counter - 1"
    203            " WHERE truth_uuid=$1"
    204            "   AND code=$2"
    205            "   AND retry_counter != 0;");
    206   /* Only the newest code is answerable.  A truth can have several unexpired
    207      codes at once, and without the LIMIT every one of them that does not
    208      match burns a retry -- so a single wrong guess could exhaust the counter,
    209      and even a correct answer cost retries for its siblings.  This matches
    210      what insert_challenge_code.c already assumes when it issues a code.
    211      Note that exhausted codes are deliberately *not* filtered out here:
    212      doing so would fall through to the newest sibling that still has
    213      attempts left, which is exactly the multiplication of the retry budget
    214      that the LIMIT is there to prevent.  The caller instead learns that the
    215      client is rate limited until the next code is issued. */
    216   PREPARE ("do_verify_challenge_code_select",
    217            "SELECT "
    218            " code"
    219            ",retry_counter"
    220            ",satisfied"
    221            " FROM anastasis_challengecode"
    222            " WHERE truth_uuid=$1"
    223            "   AND expiration_date > $2"
    224            " ORDER BY creation_date DESC"
    225            " LIMIT 1;");
    226   qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
    227                                              "do_verify_challenge_code_select",
    228                                              params,
    229                                              &check_valid_code,
    230                                              &cvc);
    231   if ( (qs < 0) ||
    232        (cvc.db_failure) )
    233     return ANASTASIS_DB_CODE_STATUS_HARD_ERROR;
    234   if (cvc.exhausted)
    235     return ANASTASIS_DB_CODE_STATUS_RATE_LIMITED;
    236   *code = cvc.code;
    237   if (cvc.valid)
    238   {
    239     *satisfied = cvc.satisfied;
    240     return ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED;
    241   }
    242   if (0 == qs)
    243     return ANASTASIS_DB_CODE_STATUS_NO_RESULTS;
    244   return ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH;
    245 }
    246 
    247 
    248 /* end of do_verify_challenge_code.c */