anastasis

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

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 }