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 */