anastasis_authorization_plugin_file.c (14312B)
1 /* 2 This file is part of Anastasis 3 Copyright (C) 2019 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 anastasis_authorization_plugin_file.c 18 * @brief authorization plugin file based for testing 19 * @author Dominik Meister 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 27 /** 28 * How many retries do we allow per code? 29 */ 30 #define INITIAL_RETRY_COUNTER 3 31 32 33 /** 34 * Context of the plugin, shared by all challenges it serves. 35 */ 36 struct FileContext 37 { 38 /** 39 * Configuration we are using. 40 */ 41 const struct ANASTASIS_AuthorizationContext *ac; 42 43 /** 44 * Directory challenge files are written to. Always ends in '/'. 45 */ 46 char *directory; 47 }; 48 49 50 /** 51 * Check that @a data names a file this plugin is allowed to write. 52 * 53 * The "address" of this method is a file name, and it arrives with the 54 * truth, i.e. it is chosen by whoever uploaded the truth and must be 55 * treated as hostile: without this check any client could make us write 56 * to an arbitrary path. Accepted are paths inside @a ctx->directory that 57 * cannot climb back out of it again. 58 * 59 * @param ctx our plugin context 60 * @param data the file name to check, not necessarily 0-terminated 61 * @param data_length number of bytes in @a data 62 * @return true if @a data may be written to 63 */ 64 static bool 65 filename_ok (const struct FileContext *ctx, 66 const char *data, 67 size_t data_length) 68 { 69 size_t dlen = strlen (ctx->directory); 70 const char *rest; 71 bool ok; 72 char *fn; 73 74 if (0 == data_length) 75 return false; 76 /* An embedded 0 would make GNUNET_strndup() keep a different (shorter) 77 name than the one checked here, so validate and start would disagree. */ 78 if (NULL != memchr (data, 79 '\0', 80 data_length)) 81 return false; 82 if (data_length <= dlen) 83 return false; 84 if (0 != strncmp (data, 85 ctx->directory, 86 dlen)) 87 return false; 88 if ('/' == data[data_length - 1]) 89 return false; 90 fn = GNUNET_strndup (data, 91 data_length); 92 /* ctx->directory ends in '/', so a leading ".." shows up as "/.." here */ 93 rest = fn + dlen - 1; 94 { 95 size_t rlen = strlen (rest); 96 97 ok = ( (NULL == strstr (rest, 98 "/../")) && 99 ( (rlen < 3) || 100 (0 != strcmp (rest + rlen - 3, 101 "/..")) ) ); 102 } 103 GNUNET_free (fn); 104 return ok; 105 } 106 107 108 /** 109 * Saves the state of a authorization process 110 */ 111 struct ANASTASIS_AUTHORIZATION_State 112 { 113 /** 114 * UUID of the challenge which is authorised 115 */ 116 struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; 117 118 /** 119 * Code which is sent to the user (here saved into a file) 120 */ 121 uint64_t code; 122 123 /** 124 * holds the truth information 125 */ 126 char *filename; 127 128 /** 129 * closure 130 */ 131 void *cls; 132 }; 133 134 135 /** 136 * Validate @a data is a well-formed input into the challenge method, 137 * i.e. @a data is a well-formed phone number for sending an SMS, or 138 * a well-formed e-mail address for sending an e-mail. Not expected to 139 * check that the phone number or e-mail account actually exists. 140 * 141 * To be possibly used before issuing a 402 payment required to the client. 142 * 143 * @param cls closure with a `const struct FileContext *` 144 * @param connection HTTP client request (for queuing response) 145 * @param truth_mime mime type of @e data 146 * @param data input to validate (i.e. is it a valid phone number, etc.) 147 * @param data_length number of bytes in @a data 148 * @return #GNUNET_OK if @a data is valid, 149 * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection 150 * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection 151 */ 152 static enum GNUNET_GenericReturnValue 153 file_validate (void *cls, 154 struct MHD_Connection *connection, 155 const char *truth_mime, 156 const char *data, 157 size_t data_length) 158 { 159 const struct FileContext *ctx = cls; 160 161 if (NULL == data) 162 return GNUNET_SYSERR; 163 /* Screen exactly the bytes file_start() will use as the file name. The 164 old check ran over the Crockford base32 *encoding* of the truth, whose 165 alphabet contains neither ' ' nor '/', so it could never reject 166 anything, while the name actually opened went unchecked. */ 167 if (! filename_ok (ctx, 168 data, 169 data_length)) 170 { 171 /* Invalid input is #GNUNET_NO with a reply queued; #GNUNET_SYSERR is 172 reserved for "invalid, and we could not even answer". */ 173 if (MHD_NO == 174 TALER_MHD_reply_with_error (connection, 175 MHD_HTTP_CONFLICT, 176 TALER_EC_GENERIC_PARAMETER_MALFORMED, 177 "filename")) 178 return GNUNET_SYSERR; 179 return GNUNET_NO; 180 } 181 return GNUNET_OK; 182 } 183 184 185 /** 186 * Begin issuing authentication challenge to user based on @a data. 187 * I.e. start to send SMS or e-mail or launch video identification. 188 * 189 * @param cls closure with a `const struct FileContext *` 190 * @param trigger function to call when we made progress 191 * @param trigger_cls closure for @a trigger 192 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the 193 * interaction with the user 194 * @param code secret code that the user has to provide back to satisfy the challenge in 195 * the main anastasis protocol 196 * @param data input to validate (i.e. is it a valid phone number, etc.) 197 * @param data_length number of bytes in @a data 198 * @return state to track progress on the authorization operation, NULL on failure 199 */ 200 static struct ANASTASIS_AUTHORIZATION_State * 201 file_start (void *cls, 202 GNUNET_SCHEDULER_TaskCallback trigger, 203 void *trigger_cls, 204 const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, 205 uint64_t code, 206 const void *data, 207 size_t data_length) 208 { 209 struct ANASTASIS_AUTHORIZATION_State *as; 210 enum GNUNET_DB_QueryStatus qs; 211 const struct FileContext *ctx = cls; 212 213 /* @e validate is optional as far as the plugin API is concerned, so the 214 screen has to be repeated here: this is the function whose result is 215 actually opened for writing. */ 216 if (! filename_ok (ctx, 217 data, 218 data_length)) 219 { 220 GNUNET_break_op (0); 221 return NULL; 222 } 223 /* If the user can show this challenge code, this 224 plugin is already happy (no additional 225 requirements), so mark this challenge as 226 already satisfied from the start. */ 227 qs = ANASTASIS_DB_update_to_challenge_code_satisfied ( 228 truth_uuid, 229 code); 230 if (qs <= 0) 231 { 232 GNUNET_break (0); 233 return NULL; 234 } 235 as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); 236 as->cls = cls; 237 as->truth_uuid = *truth_uuid; 238 as->code = code; 239 as->filename = GNUNET_strndup (data, 240 data_length); 241 return as; 242 } 243 244 245 /** 246 * Begin issuing authentication challenge to user based on @a data. 247 * I.e. start to send SMS or e-mail or launch video identification. 248 * 249 * @param as authorization state 250 * @param connection HTTP client request (for queuing response, such as redirection to video portal) 251 * @return state of the request 252 */ 253 static enum ANASTASIS_AUTHORIZATION_ChallengeResult 254 file_challenge (struct ANASTASIS_AUTHORIZATION_State *as, 255 struct MHD_Connection *connection) 256 { 257 const char *mime; 258 const char *lang; 259 260 mime = MHD_lookup_connection_value (connection, 261 MHD_HEADER_KIND, 262 MHD_HTTP_HEADER_ACCEPT); 263 if (NULL == mime) 264 mime = "text/plain"; 265 lang = MHD_lookup_connection_value (connection, 266 MHD_HEADER_KIND, 267 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 268 if (NULL == lang) 269 lang = "en"; 270 { 271 FILE *f = fopen (as->filename, "w"); 272 273 if (NULL == f) 274 { 275 struct MHD_Response *resp; 276 enum MHD_Result mres; 277 278 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 279 "open", 280 as->filename); 281 resp = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 282 "open"); 283 mres = MHD_queue_response (connection, 284 MHD_HTTP_INTERNAL_SERVER_ERROR, 285 resp); 286 MHD_destroy_response (resp); 287 if (MHD_YES != mres) 288 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 289 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 290 } 291 292 /* print challenge code to file */ 293 if (0 >= fprintf (f, 294 "%lu", 295 as->code)) 296 { 297 struct MHD_Response *resp; 298 enum MHD_Result mres; 299 300 GNUNET_break (0 == fclose (f)); 301 resp = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 302 "write"); 303 mres = MHD_queue_response (connection, 304 MHD_HTTP_INTERNAL_SERVER_ERROR, 305 resp); 306 MHD_destroy_response (resp); 307 if (MHD_YES != mres) 308 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 309 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 310 } 311 GNUNET_break (0 == fclose (f)); 312 } 313 314 /* Build HTTP response */ 315 { 316 struct MHD_Response *resp; 317 318 if (0.0 < TALER_pattern_matches (mime, 319 "application/json")) 320 { 321 resp = TALER_MHD_MAKE_JSON_PACK ( 322 GNUNET_JSON_pack_string ("challenge_type", 323 "FILE_WRITTEN"), 324 GNUNET_JSON_pack_string ("filename", 325 as->filename)); 326 } 327 else 328 { 329 size_t response_size; 330 char *response; 331 332 response_size = GNUNET_asprintf (&response, 333 _ ("Challenge written to file")); 334 resp = MHD_create_response_from_buffer (response_size, 335 response, 336 MHD_RESPMEM_MUST_COPY); 337 GNUNET_free (response); 338 TALER_MHD_add_global_headers (resp, 339 false); 340 GNUNET_break (MHD_YES == 341 MHD_add_response_header (resp, 342 MHD_HTTP_HEADER_CONTENT_TYPE, 343 "text/plain")); 344 } 345 346 { 347 enum MHD_Result mres; 348 349 mres = MHD_queue_response (connection, 350 MHD_HTTP_OK, 351 resp); 352 MHD_destroy_response (resp); 353 if (MHD_YES != mres) 354 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED; 355 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS; 356 } 357 } 358 } 359 360 361 /** 362 * Free internal state associated with @a as. 363 * 364 * @param as state to clean up 365 */ 366 static void 367 file_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) 368 { 369 GNUNET_free (as->filename); 370 GNUNET_free (as); 371 } 372 373 374 /** 375 * Initialize File based authorization plugin 376 * 377 * @param cls a configuration instance 378 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` 379 */ 380 void * 381 libanastasis_plugin_authorization_file_init (void *cls); 382 383 /* declaration to fix compiler warning */ 384 void * 385 libanastasis_plugin_authorization_file_init (void *cls) 386 { 387 const struct ANASTASIS_AuthorizationContext *ac = cls; 388 struct ANASTASIS_AuthorizationPlugin *plugin; 389 struct FileContext *ctx; 390 char *dir; 391 392 if (GNUNET_OK != 393 GNUNET_CONFIGURATION_get_value_filename (ac->cfg, 394 "authorization-file", 395 "DIRECTORY", 396 &dir)) 397 { 398 const char *tmpdir = getenv ("TMPDIR"); 399 400 if (NULL == tmpdir) 401 tmpdir = "/tmp"; 402 GNUNET_asprintf (&dir, 403 "%s/anastasis-file-challenges", 404 tmpdir); 405 } 406 ctx = GNUNET_new (struct FileContext); 407 ctx->ac = ac; 408 /* filename_ok() relies on the trailing '/' to tell "inside the directory" 409 from "a sibling whose name merely starts the same way". */ 410 GNUNET_asprintf (&ctx->directory, 411 "%s/", 412 dir); 413 GNUNET_free (dir); 414 if (GNUNET_OK != 415 GNUNET_DISK_directory_create (ctx->directory)) 416 { 417 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 418 "Failed to create directory `%s' for file challenges\n", 419 ctx->directory); 420 GNUNET_free (ctx->directory); 421 GNUNET_free (ctx); 422 return NULL; 423 } 424 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); 425 plugin->cls = ctx; 426 plugin->retry_counter = INITIAL_RETRY_COUNTER; 427 plugin->code_validity_period = GNUNET_TIME_UNIT_MINUTES; 428 plugin->code_rotation_period = GNUNET_TIME_UNIT_MINUTES; 429 plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; 430 plugin->validate = &file_validate; 431 plugin->start = &file_start; 432 plugin->challenge = &file_challenge; 433 plugin->cleanup = &file_cleanup; 434 return plugin; 435 } 436 437 438 /** 439 * Unload authorization plugin 440 * 441 * @param cls a `struct ANASTASIS_AuthorizationPlugin` 442 * @return NULL (always) 443 */ 444 void * 445 libanastasis_plugin_authorization_file_done (void *cls); 446 447 /* declaration to fix compiler warning */ 448 void * 449 libanastasis_plugin_authorization_file_done (void *cls) 450 { 451 struct ANASTASIS_AuthorizationPlugin *plugin = cls; 452 struct FileContext *ctx = plugin->cls; 453 454 GNUNET_free (ctx->directory); 455 GNUNET_free (ctx); 456 GNUNET_free (plugin); 457 return NULL; 458 }