anastasis_authorization_plugin_totp.c (13879B)
1 /* 2 This totp is part of Anastasis 3 Copyright (C) 2021 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 totp COPYING.GPL. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @totp anastasis_authorization_plugin_totp.c 18 * @brief authorization plugin using totp 19 * @author Christian Grothoff 20 */ 21 #include "platform.h" 22 #include "anastasis_authorization_plugin.h" 23 #include <taler/taler_mhd_lib.h> 24 #include <gnunet/gnunet_db_lib.h> 25 #include "anastasis_database_lib.h" 26 #include <gcrypt.h> 27 28 29 /** 30 * How many retries do we allow per code? 31 */ 32 #define INITIAL_RETRY_COUNTER 3 33 34 /** 35 * How long is a TOTP code valid? 36 */ 37 #define TOTP_VALIDITY_PERIOD GNUNET_TIME_relative_multiply ( \ 38 GNUNET_TIME_UNIT_SECONDS, 30) 39 40 /** 41 * Range of time we allow (plus-minus). 42 */ 43 #define TIME_INTERVAL_RANGE 2 44 45 /** 46 * How long is the shared secret in bytes? 47 */ 48 #define SECRET_LEN 32 49 50 51 /** 52 * Saves the state of a authorization process 53 */ 54 struct ANASTASIS_AUTHORIZATION_State 55 { 56 /** 57 * UUID of the challenge which is authorised 58 */ 59 struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; 60 61 /** 62 * Was the challenge satisfied? 63 */ 64 struct GNUNET_HashCode valid_replies[TIME_INTERVAL_RANGE * 2 + 1]; 65 66 /** 67 * Time step counter each entry of @e valid_replies was computed for, 68 * in the same order. Needed to tell a replay from a fresh code. 69 */ 70 uint64_t counters[TIME_INTERVAL_RANGE * 2 + 1]; 71 72 /** 73 * Our context. 74 */ 75 const struct ANASTASIS_AuthorizationContext *ac; 76 77 }; 78 79 80 /** 81 * Validate @a data is a well-formed input into the challenge method, 82 * i.e. @a data is a well-formed phone number for sending an SMS, or 83 * a well-formed e-mail address for sending an e-mail. Not expected to 84 * check that the phone number or e-mail account actually exists. 85 * 86 * To be possibly used before issuing a 402 payment required to the client. 87 * 88 * @param cls closure with a `const struct ANASTASIS_AuthorizationContext *` 89 * @param connection HTTP client request (for queuing response) 90 * @param truth_mime mime type of @e data 91 * @param data input to validate (i.e. is it a valid phone number, etc.) 92 * @param data_length number of bytes in @a data 93 * @return #GNUNET_OK if @a data is valid, 94 * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection 95 * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection 96 */ 97 static enum GNUNET_GenericReturnValue 98 totp_validate (void *cls, 99 struct MHD_Connection *connection, 100 const char *truth_mime, 101 const char *data, 102 size_t data_length) 103 { 104 (void) cls; 105 (void) truth_mime; 106 (void) connection; 107 if (NULL == data) 108 { 109 GNUNET_break_op (0); 110 if (MHD_NO == 111 TALER_MHD_reply_with_error (connection, 112 MHD_HTTP_CONFLICT, 113 TALER_EC_ANASTASIS_TOTP_KEY_MISSING, 114 NULL)) 115 return GNUNET_SYSERR; 116 return GNUNET_NO; 117 } 118 if (SECRET_LEN != data_length) 119 { 120 GNUNET_break_op (0); 121 if (MHD_NO == 122 TALER_MHD_reply_with_error (connection, 123 MHD_HTTP_CONFLICT, 124 TALER_EC_ANASTASIS_TOTP_KEY_INVALID, 125 NULL)) 126 return GNUNET_SYSERR; 127 return GNUNET_NO; 128 } 129 return GNUNET_OK; 130 } 131 132 133 /** 134 * Compute TOTP code at current time with offset 135 * @a time_off for the @a key. 136 * 137 * @param time_off offset to apply when computing the code 138 * @param key input key material 139 * @param key_size number of bytes in @a key 140 * @param[out] counter set to the time step counter the code belongs to 141 * @return TOTP code at this time 142 */ 143 static uint64_t 144 compute_totp (int time_off, 145 const void *key, 146 size_t key_size, 147 uint64_t *counter) 148 { 149 struct GNUNET_TIME_Absolute now; 150 time_t t; 151 uint64_t ctr; 152 uint8_t hmac[20]; /* SHA1: 20 bytes */ 153 154 now = GNUNET_TIME_absolute_get (); 155 while (time_off < 0) 156 { 157 now = GNUNET_TIME_absolute_subtract (now, 158 TOTP_VALIDITY_PERIOD); 159 time_off++; 160 } 161 while (time_off > 0) 162 { 163 now = GNUNET_TIME_absolute_add (now, 164 TOTP_VALIDITY_PERIOD); 165 time_off--; 166 } 167 t = now.abs_value_us / GNUNET_TIME_UNIT_SECONDS.rel_value_us; 168 *counter = t / 30LLU; 169 ctr = GNUNET_htonll (*counter); 170 171 { 172 gcry_md_hd_t md; 173 const unsigned char *mc; 174 175 GNUNET_assert (GPG_ERR_NO_ERROR == 176 gcry_md_open (&md, 177 GCRY_MD_SHA1, 178 GCRY_MD_FLAG_HMAC)); 179 GNUNET_assert (GPG_ERR_NO_ERROR == 180 gcry_md_setkey (md, 181 key, 182 key_size)); 183 gcry_md_write (md, 184 &ctr, 185 sizeof (ctr)); 186 mc = gcry_md_read (md, 187 GCRY_MD_SHA1); 188 GNUNET_assert (NULL != mc); 189 memcpy (hmac, 190 mc, 191 sizeof (hmac)); 192 gcry_md_close (md); 193 } 194 195 { 196 uint32_t code = 0; 197 int offset; 198 199 offset = hmac[sizeof (hmac) - 1] & 0x0f; 200 for (int count = 0; count < 4; count++) 201 code |= ((uint32_t) hmac[offset + 3 - count]) << (8 * count); 202 code &= 0x7fffffff; 203 /* always use 8 digits (maximum) */ 204 code = code % 100000000; 205 return code; 206 } 207 } 208 209 210 /** 211 * Begin issuing authentication challenge to user based on @a data. 212 * 213 * @param cls closure 214 * @param trigger function to call when we made progress 215 * @param trigger_cls closure for @a trigger 216 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the 217 * interaction with the user 218 * @param code always 0 (direct validation, backend does 219 * not generate a code in this mode) 220 * @param data truth for input to validate (i.e. the shared secret) 221 * @param data_length number of bytes in @a data 222 * @return state to track progress on the authorization operation, NULL on failure 223 */ 224 static struct ANASTASIS_AUTHORIZATION_State * 225 totp_start (void *cls, 226 GNUNET_SCHEDULER_TaskCallback trigger, 227 void *trigger_cls, 228 const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, 229 uint64_t code, 230 const void *data, 231 size_t data_length) 232 { 233 const struct ANASTASIS_AuthorizationContext *ac = cls; 234 struct ANASTASIS_AUTHORIZATION_State *as; 235 uint64_t want; 236 unsigned int off = 0; 237 238 GNUNET_assert (0 == code); 239 if (SECRET_LEN != data_length) 240 { 241 /* totp_validate() enforces this on upload, but the truth we are handed 242 here comes back from the database, so do not trust its length. */ 243 GNUNET_break_op (0); 244 return NULL; 245 } 246 as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); 247 as->ac = ac; 248 as->truth_uuid = *truth_uuid; 249 for (int i = -TIME_INTERVAL_RANGE; 250 i <= TIME_INTERVAL_RANGE; 251 i++) 252 { 253 want = compute_totp (i, 254 data, 255 data_length, 256 &as->counters[off]); 257 ANASTASIS_hash_answer (want, 258 &as->valid_replies[off++]); 259 } 260 return as; 261 } 262 263 264 /** 265 * Check authentication response from the user. 266 * 267 * @param as authorization state 268 * @param timeout how long do we have to produce a reply 269 * @param challenge_response hash of the response 270 * @param connection HTTP client request (for queuing response, such as redirection to video portal) 271 * @return state of the request 272 */ 273 static enum ANASTASIS_AUTHORIZATION_SolveResult 274 totp_solve (struct ANASTASIS_AUTHORIZATION_State *as, 275 struct GNUNET_TIME_Absolute timeout, 276 const struct GNUNET_HashCode *challenge_response, 277 struct MHD_Connection *connection) 278 { 279 enum MHD_Result mres; 280 const char *mime; 281 const char *lang; 282 283 for (unsigned int i = 0; i<=TIME_INTERVAL_RANGE * 2; i++) 284 { 285 enum GNUNET_DB_QueryStatus qs; 286 287 if (0 != 288 GNUNET_memcmp (challenge_response, 289 &as->valid_replies[i])) 290 continue; 291 /* The code is right for time step @e counters[i], but a code stays right 292 for as long as its window lasts, so anyone who saw it could send it 293 again. Consume the time step: this succeeds at most once (RFC 6238, 294 section 5.2), and because the check and the store are the same 295 statement two requests carrying the same code cannot both win. */ 296 qs = ANASTASIS_DB_update_totp_counter (&as->truth_uuid, 297 as->counters[i]); 298 if (qs < 0) 299 { 300 GNUNET_break (0); 301 if (MHD_NO == 302 TALER_MHD_reply_with_error (connection, 303 MHD_HTTP_INTERNAL_SERVER_ERROR, 304 TALER_EC_GENERIC_DB_STORE_FAILED, 305 "update_totp_counter")) 306 return ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED; 307 return ANASTASIS_AUTHORIZATION_SRES_FAILED; 308 } 309 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) 310 { 311 /* Replay of a code we already honoured (or the truth is gone by now). 312 Answer exactly as for a wrong code, so that the client learns 313 nothing from which of the two it was. */ 314 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 315 "Refusing TOTP code for time step %llu of truth %s: already used\n", 316 (unsigned long long) as->counters[i], 317 TALER_B2S (&as->truth_uuid)); 318 break; 319 } 320 return ANASTASIS_AUTHORIZATION_SRES_FINISHED; 321 } 322 mime = MHD_lookup_connection_value (connection, 323 MHD_HEADER_KIND, 324 MHD_HTTP_HEADER_ACCEPT); 325 if (NULL == mime) 326 mime = "text/plain"; 327 lang = MHD_lookup_connection_value (connection, 328 MHD_HEADER_KIND, 329 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 330 if (NULL == lang) 331 lang = "en"; 332 333 /* Build HTTP response */ 334 { 335 struct MHD_Response *resp; 336 struct GNUNET_TIME_Timestamp now; 337 338 now = GNUNET_TIME_timestamp_get (); 339 if (0.0 < TALER_pattern_matches (mime, 340 "application/json")) 341 { 342 resp = TALER_MHD_MAKE_JSON_PACK ( 343 GNUNET_JSON_pack_uint64 ("code", 344 TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED), 345 GNUNET_JSON_pack_string ("hint", 346 TALER_ErrorCode_get_hint ( 347 TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED)), 348 GNUNET_JSON_pack_timestamp ("server_time", 349 now)); 350 } 351 else 352 { 353 size_t response_size; 354 char *response; 355 356 // FIXME: i18n of the message based on 'lang' ... 357 response_size 358 = GNUNET_asprintf (&response, 359 "Server time: %s", 360 GNUNET_TIME_timestamp2s (now)); 361 resp = MHD_create_response_from_buffer (response_size, 362 response, 363 MHD_RESPMEM_MUST_COPY); 364 TALER_MHD_add_global_headers (resp, 365 false); 366 GNUNET_break (MHD_YES == 367 MHD_add_response_header (resp, 368 MHD_HTTP_HEADER_CONTENT_TYPE, 369 "text/plain")); 370 } 371 mres = MHD_queue_response (connection, 372 MHD_HTTP_FORBIDDEN, 373 resp); 374 MHD_destroy_response (resp); 375 } 376 if (MHD_YES != mres) 377 return ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED; 378 return ANASTASIS_AUTHORIZATION_SRES_FAILED; 379 } 380 381 382 /** 383 * Free internal state associated with @a as. 384 * 385 * @param as state to clean up 386 */ 387 static void 388 totp_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) 389 { 390 GNUNET_free (as); 391 } 392 393 394 /** 395 * Initialize Totp based authorization plugin 396 * 397 * @param cls a configuration instance 398 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` 399 */ 400 void * 401 libanastasis_plugin_authorization_totp_init (void *cls); 402 403 /* declaration to fix compiler warning */ 404 void * 405 libanastasis_plugin_authorization_totp_init (void *cls) 406 { 407 const struct ANASTASIS_AuthorizationContext *ac = cls; 408 struct ANASTASIS_AuthorizationPlugin *plugin; 409 410 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); 411 plugin->cls = (void *) ac; 412 plugin->user_provided_code = true; 413 plugin->retry_counter = INITIAL_RETRY_COUNTER; 414 plugin->code_validity_period = TOTP_VALIDITY_PERIOD; 415 plugin->code_rotation_period = plugin->code_validity_period; 416 plugin->code_retransmission_frequency = plugin->code_validity_period; 417 plugin->validate = &totp_validate; 418 plugin->start = &totp_start; 419 plugin->solve = &totp_solve; 420 plugin->cleanup = &totp_cleanup; 421 return plugin; 422 } 423 424 425 /** 426 * Unload authorization plugin 427 * 428 * @param cls a `struct ANASTASIS_AuthorizationPlugin` 429 * @return NULL (always) 430 */ 431 void * 432 libanastasis_plugin_authorization_totp_done (void *cls); 433 434 /* declaration to fix compiler warning */ 435 void * 436 libanastasis_plugin_authorization_totp_done (void *cls) 437 { 438 struct ANASTASIS_AuthorizationPlugin *plugin = cls; 439 440 GNUNET_free (plugin); 441 return NULL; 442 }