anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

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 }