anastasis_authorization_plugin_totp.c (11764B)
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 * Our context. 68 */ 69 const struct ANASTASIS_AuthorizationContext *ac; 70 71 }; 72 73 74 /** 75 * Validate @a data is a well-formed input into the challenge method, 76 * i.e. @a data is a well-formed phone number for sending an SMS, or 77 * a well-formed e-mail address for sending an e-mail. Not expected to 78 * check that the phone number or e-mail account actually exists. 79 * 80 * To be possibly used before issuing a 402 payment required to the client. 81 * 82 * @param cls closure with a `const struct ANASTASIS_AuthorizationContext *` 83 * @param connection HTTP client request (for queuing response) 84 * @param truth_mime mime type of @e data 85 * @param data input to validate (i.e. is it a valid phone number, etc.) 86 * @param data_length number of bytes in @a data 87 * @return #GNUNET_OK if @a data is valid, 88 * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection 89 * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection 90 */ 91 static enum GNUNET_GenericReturnValue 92 totp_validate (void *cls, 93 struct MHD_Connection *connection, 94 const char *truth_mime, 95 const char *data, 96 size_t data_length) 97 { 98 (void) cls; 99 (void) truth_mime; 100 (void) connection; 101 if (NULL == data) 102 { 103 GNUNET_break_op (0); 104 if (MHD_NO == 105 TALER_MHD_reply_with_error (connection, 106 MHD_HTTP_CONFLICT, 107 TALER_EC_ANASTASIS_TOTP_KEY_MISSING, 108 NULL)) 109 return GNUNET_SYSERR; 110 return GNUNET_NO; 111 } 112 if (SECRET_LEN != data_length) 113 { 114 GNUNET_break_op (0); 115 if (MHD_NO == 116 TALER_MHD_reply_with_error (connection, 117 MHD_HTTP_CONFLICT, 118 TALER_EC_ANASTASIS_TOTP_KEY_INVALID, 119 NULL)) 120 return GNUNET_SYSERR; 121 return GNUNET_NO; 122 } 123 return GNUNET_OK; 124 } 125 126 127 /** 128 * Compute TOTP code at current time with offset 129 * @a time_off for the @a key. 130 * 131 * @param time_off offset to apply when computing the code 132 * @param key input key material 133 * @param key_size number of bytes in @a key 134 * @return TOTP code at this time 135 */ 136 static uint64_t 137 compute_totp (int time_off, 138 const void *key, 139 size_t key_size) 140 { 141 struct GNUNET_TIME_Absolute now; 142 time_t t; 143 uint64_t ctr; 144 uint8_t hmac[20]; /* SHA1: 20 bytes */ 145 146 now = GNUNET_TIME_absolute_get (); 147 while (time_off < 0) 148 { 149 now = GNUNET_TIME_absolute_subtract (now, 150 TOTP_VALIDITY_PERIOD); 151 time_off++; 152 } 153 while (time_off > 0) 154 { 155 now = GNUNET_TIME_absolute_add (now, 156 TOTP_VALIDITY_PERIOD); 157 time_off--; 158 } 159 t = now.abs_value_us / GNUNET_TIME_UNIT_SECONDS.rel_value_us; 160 ctr = GNUNET_htonll (t / 30LLU); 161 162 { 163 gcry_md_hd_t md; 164 const unsigned char *mc; 165 166 GNUNET_assert (GPG_ERR_NO_ERROR == 167 gcry_md_open (&md, 168 GCRY_MD_SHA1, 169 GCRY_MD_FLAG_HMAC)); 170 GNUNET_assert (GPG_ERR_NO_ERROR == 171 gcry_md_setkey (md, 172 key, 173 key_size)); 174 gcry_md_write (md, 175 &ctr, 176 sizeof (ctr)); 177 mc = gcry_md_read (md, 178 GCRY_MD_SHA1); 179 GNUNET_assert (NULL != mc); 180 memcpy (hmac, 181 mc, 182 sizeof (hmac)); 183 gcry_md_close (md); 184 } 185 186 { 187 uint32_t code = 0; 188 int offset; 189 190 offset = hmac[sizeof (hmac) - 1] & 0x0f; 191 for (int count = 0; count < 4; count++) 192 code |= ((uint32_t) hmac[offset + 3 - count]) << (8 * count); 193 code &= 0x7fffffff; 194 /* always use 8 digits (maximum) */ 195 code = code % 100000000; 196 return code; 197 } 198 } 199 200 201 /** 202 * Begin issuing authentication challenge to user based on @a data. 203 * 204 * @param cls closure 205 * @param trigger function to call when we made progress 206 * @param trigger_cls closure for @a trigger 207 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the 208 * interaction with the user 209 * @param code always 0 (direct validation, backend does 210 * not generate a code in this mode) 211 * @param data truth for input to validate (i.e. the shared secret) 212 * @param data_length number of bytes in @a data 213 * @return state to track progress on the authorization operation, NULL on failure 214 */ 215 static struct ANASTASIS_AUTHORIZATION_State * 216 totp_start (void *cls, 217 GNUNET_SCHEDULER_TaskCallback trigger, 218 void *trigger_cls, 219 const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, 220 uint64_t code, 221 const void *data, 222 size_t data_length) 223 { 224 const struct ANASTASIS_AuthorizationContext *ac = cls; 225 struct ANASTASIS_AUTHORIZATION_State *as; 226 uint64_t want; 227 unsigned int off = 0; 228 229 GNUNET_assert (0 == code); 230 as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); 231 as->ac = ac; 232 as->truth_uuid = *truth_uuid; 233 for (int i = -TIME_INTERVAL_RANGE; 234 i <= TIME_INTERVAL_RANGE; 235 i++) 236 { 237 want = compute_totp (i, 238 data, 239 data_length); 240 ANASTASIS_hash_answer (want, 241 &as->valid_replies[off++]); 242 } 243 return as; 244 } 245 246 247 /** 248 * Check authentication response from the user. 249 * 250 * @param as authorization state 251 * @param timeout how long do we have to produce a reply 252 * @param challenge_response hash of the response 253 * @param connection HTTP client request (for queuing response, such as redirection to video portal) 254 * @return state of the request 255 */ 256 static enum ANASTASIS_AUTHORIZATION_SolveResult 257 totp_solve (struct ANASTASIS_AUTHORIZATION_State *as, 258 struct GNUNET_TIME_Absolute timeout, 259 const struct GNUNET_HashCode *challenge_response, 260 struct MHD_Connection *connection) 261 { 262 MHD_RESULT mres; 263 const char *mime; 264 const char *lang; 265 266 for (unsigned int i = 0; i<=TIME_INTERVAL_RANGE * 2; i++) 267 if (0 == 268 GNUNET_memcmp (challenge_response, 269 &as->valid_replies[i])) 270 return ANASTASIS_AUTHORIZATION_SRES_FINISHED; 271 mime = MHD_lookup_connection_value (connection, 272 MHD_HEADER_KIND, 273 MHD_HTTP_HEADER_ACCEPT); 274 if (NULL == mime) 275 mime = "text/plain"; 276 lang = MHD_lookup_connection_value (connection, 277 MHD_HEADER_KIND, 278 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 279 if (NULL == lang) 280 lang = "en"; 281 282 /* Build HTTP response */ 283 { 284 struct MHD_Response *resp; 285 struct GNUNET_TIME_Timestamp now; 286 287 now = GNUNET_TIME_timestamp_get (); 288 if (0.0 < TALER_pattern_matches (mime, 289 "application/json")) 290 { 291 resp = TALER_MHD_MAKE_JSON_PACK ( 292 GNUNET_JSON_pack_uint64 ("code", 293 TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED), 294 GNUNET_JSON_pack_string ("hint", 295 TALER_ErrorCode_get_hint ( 296 TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED)), 297 GNUNET_JSON_pack_timestamp ("server_time", 298 now)); 299 } 300 else 301 { 302 size_t response_size; 303 char *response; 304 305 // FIXME: i18n of the message based on 'lang' ... 306 response_size 307 = GNUNET_asprintf (&response, 308 "Server time: %s", 309 GNUNET_TIME_timestamp2s (now)); 310 resp = MHD_create_response_from_buffer (response_size, 311 response, 312 MHD_RESPMEM_MUST_COPY); 313 TALER_MHD_add_global_headers (resp, 314 false); 315 GNUNET_break (MHD_YES == 316 MHD_add_response_header (resp, 317 MHD_HTTP_HEADER_CONTENT_TYPE, 318 "text/plain")); 319 } 320 mres = MHD_queue_response (connection, 321 MHD_HTTP_FORBIDDEN, 322 resp); 323 MHD_destroy_response (resp); 324 } 325 if (MHD_YES != mres) 326 return ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED; 327 return ANASTASIS_AUTHORIZATION_SRES_FAILED; 328 } 329 330 331 /** 332 * Free internal state associated with @a as. 333 * 334 * @param as state to clean up 335 */ 336 static void 337 totp_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) 338 { 339 GNUNET_free (as); 340 } 341 342 343 /** 344 * Initialize Totp based authorization plugin 345 * 346 * @param cls a configuration instance 347 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` 348 */ 349 void * 350 libanastasis_plugin_authorization_totp_init (void *cls); 351 352 /* declaration to fix compiler warning */ 353 void * 354 libanastasis_plugin_authorization_totp_init (void *cls) 355 { 356 const struct ANASTASIS_AuthorizationContext *ac = cls; 357 struct ANASTASIS_AuthorizationPlugin *plugin; 358 359 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); 360 plugin->cls = (void *) ac; 361 plugin->user_provided_code = true; 362 plugin->retry_counter = INITIAL_RETRY_COUNTER; 363 plugin->code_validity_period = TOTP_VALIDITY_PERIOD; 364 plugin->code_rotation_period = plugin->code_validity_period; 365 plugin->code_retransmission_frequency = plugin->code_validity_period; 366 plugin->validate = &totp_validate; 367 plugin->start = &totp_start; 368 plugin->solve = &totp_solve; 369 plugin->cleanup = &totp_cleanup; 370 return plugin; 371 } 372 373 374 /** 375 * Unload authorization plugin 376 * 377 * @param cls a `struct ANASTASIS_AuthorizationPlugin` 378 * @return NULL (always) 379 */ 380 void * 381 libanastasis_plugin_authorization_totp_done (void *cls); 382 383 /* declaration to fix compiler warning */ 384 void * 385 libanastasis_plugin_authorization_totp_done (void *cls) 386 { 387 struct ANASTASIS_AuthorizationPlugin *plugin = cls; 388 389 GNUNET_free (plugin); 390 return NULL; 391 }