anastasis_authorization_plugin_file.c (15058B)
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; 272 int fd; 273 274 /* The challenge code is a secret, and the default directory sits in a 275 world-writable /tmp: refuse to follow a symlink someone else planted 276 there, and do not let anyone else read the code back. */ 277 fd = open (as->filename, 278 O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 279 S_IRUSR | S_IWUSR); 280 f = (-1 == fd) 281 ? NULL 282 : fdopen (fd, "w"); 283 if (NULL == f) 284 { 285 struct MHD_Response *resp; 286 enum MHD_Result mres; 287 288 if (-1 != fd) 289 GNUNET_break (0 == close (fd)); 290 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 291 "open", 292 as->filename); 293 resp = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 294 "open"); 295 mres = MHD_queue_response (connection, 296 MHD_HTTP_INTERNAL_SERVER_ERROR, 297 resp); 298 MHD_destroy_response (resp); 299 if (MHD_YES != mres) 300 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 301 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 302 } 303 304 /* print challenge code to file */ 305 if (0 >= fprintf (f, 306 "%lu", 307 as->code)) 308 { 309 struct MHD_Response *resp; 310 enum MHD_Result mres; 311 312 GNUNET_break (0 == fclose (f)); 313 resp = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 314 "write"); 315 mres = MHD_queue_response (connection, 316 MHD_HTTP_INTERNAL_SERVER_ERROR, 317 resp); 318 MHD_destroy_response (resp); 319 if (MHD_YES != mres) 320 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 321 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 322 } 323 GNUNET_break (0 == fclose (f)); 324 } 325 326 /* Build HTTP response */ 327 { 328 struct MHD_Response *resp; 329 330 if (0.0 < TALER_pattern_matches (mime, 331 "application/json")) 332 { 333 resp = TALER_MHD_MAKE_JSON_PACK ( 334 GNUNET_JSON_pack_string ("challenge_type", 335 "FILE_WRITTEN"), 336 GNUNET_JSON_pack_string ("filename", 337 as->filename)); 338 } 339 else 340 { 341 size_t response_size; 342 char *response; 343 344 response_size = GNUNET_asprintf (&response, 345 _ ("Challenge written to file")); 346 resp = MHD_create_response_from_buffer (response_size, 347 response, 348 MHD_RESPMEM_MUST_COPY); 349 GNUNET_free (response); 350 TALER_MHD_add_global_headers (resp, 351 false); 352 GNUNET_break (MHD_YES == 353 MHD_add_response_header (resp, 354 MHD_HTTP_HEADER_CONTENT_TYPE, 355 "text/plain")); 356 } 357 358 { 359 enum MHD_Result mres; 360 361 mres = MHD_queue_response (connection, 362 MHD_HTTP_OK, 363 resp); 364 MHD_destroy_response (resp); 365 if (MHD_YES != mres) 366 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED; 367 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS; 368 } 369 } 370 } 371 372 373 /** 374 * Free internal state associated with @a as. 375 * 376 * @param as state to clean up 377 */ 378 static void 379 file_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) 380 { 381 GNUNET_free (as->filename); 382 GNUNET_free (as); 383 } 384 385 386 /** 387 * Initialize File based authorization plugin 388 * 389 * @param cls a configuration instance 390 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` 391 */ 392 void * 393 libanastasis_plugin_authorization_file_init (void *cls); 394 395 /* declaration to fix compiler warning */ 396 void * 397 libanastasis_plugin_authorization_file_init (void *cls) 398 { 399 const struct ANASTASIS_AuthorizationContext *ac = cls; 400 struct ANASTASIS_AuthorizationPlugin *plugin; 401 struct FileContext *ctx; 402 char *dir; 403 404 if (GNUNET_OK != 405 GNUNET_CONFIGURATION_get_value_filename (ac->cfg, 406 "authorization-file", 407 "DIRECTORY", 408 &dir)) 409 { 410 const char *tmpdir = getenv ("TMPDIR"); 411 412 if (NULL == tmpdir) 413 tmpdir = "/tmp"; 414 GNUNET_asprintf (&dir, 415 "%s/anastasis-file-challenges", 416 tmpdir); 417 } 418 ctx = GNUNET_new (struct FileContext); 419 ctx->ac = ac; 420 /* The default lands in a shared /tmp under a predictable name, so a local 421 user can pre-create it and read every challenge code that follows. That 422 is tolerable only because this plugin exists for testing; an operator who 423 points DIRECTORY somewhere private gets a private directory. */ 424 /* filename_ok() relies on the trailing '/' to tell "inside the directory" 425 from "a sibling whose name merely starts the same way". */ 426 GNUNET_asprintf (&ctx->directory, 427 "%s/", 428 dir); 429 GNUNET_free (dir); 430 if (GNUNET_OK != 431 GNUNET_DISK_directory_create (ctx->directory)) 432 { 433 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 434 "Failed to create directory `%s' for file challenges\n", 435 ctx->directory); 436 GNUNET_free (ctx->directory); 437 GNUNET_free (ctx); 438 return NULL; 439 } 440 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); 441 plugin->cls = ctx; 442 plugin->retry_counter = INITIAL_RETRY_COUNTER; 443 plugin->code_validity_period = GNUNET_TIME_UNIT_MINUTES; 444 plugin->code_rotation_period = GNUNET_TIME_UNIT_MINUTES; 445 plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; 446 plugin->validate = &file_validate; 447 plugin->start = &file_start; 448 plugin->challenge = &file_challenge; 449 plugin->cleanup = &file_cleanup; 450 return plugin; 451 } 452 453 454 /** 455 * Unload authorization plugin 456 * 457 * @param cls a `struct ANASTASIS_AuthorizationPlugin` 458 * @return NULL (always) 459 */ 460 void * 461 libanastasis_plugin_authorization_file_done (void *cls); 462 463 /* declaration to fix compiler warning */ 464 void * 465 libanastasis_plugin_authorization_file_done (void *cls) 466 { 467 struct ANASTASIS_AuthorizationPlugin *plugin = cls; 468 struct FileContext *ctx = plugin->cls; 469 470 GNUNET_free (ctx->directory); 471 GNUNET_free (ctx); 472 GNUNET_free (plugin); 473 return NULL; 474 }