anastasis

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

anastasis_authorization_plugin_post.c (20521B)


      1 /*
      2   This file 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 file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file anastasis_authorization_plugin_post.c
     18  * @brief authorization plugin post based
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"
     22 #include "anastasis_authorization_plugin.h"
     23 #include <taler/taler_mhd_lib.h>
     24 #include <taler/taler_json_lib.h>
     25 #include <jansson.h>
     26 #include "anastasis_util_lib.h"
     27 #include <gnunet/gnunet_db_lib.h>
     28 #include "anastasis_database_lib.h"
     29 
     30 /**
     31  * How many retries do we allow per code?
     32  */
     33 #define INITIAL_RETRY_COUNTER 3
     34 
     35 
     36 /**
     37  * Saves the State of a authorization plugin.
     38  */
     39 struct PostContext
     40 {
     41 
     42   /**
     43    * Command which is executed to run the plugin (some bash script or a
     44    * command line argument)
     45    */
     46   char *auth_command;
     47 
     48   /**
     49    * Messages of the plugin, read from a resource file.
     50    */
     51   json_t *messages;
     52 
     53   /**
     54    * Argument passed to the "init" function of each
     55    * plugin.
     56    */
     57   const struct ANASTASIS_AuthorizationContext *ac;
     58 };
     59 
     60 
     61 /**
     62  * Saves the state of a authorization process
     63  */
     64 struct ANASTASIS_AUTHORIZATION_State
     65 {
     66   /**
     67    * Public key of the challenge which is authorised
     68    */
     69   struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid;
     70 
     71   /**
     72    * Code which is sent to the user.
     73    */
     74   uint64_t code;
     75 
     76   /**
     77    * Our plugin context.
     78    */
     79   struct PostContext *ctx;
     80 
     81   /**
     82    * Function to call when we made progress.
     83    */
     84   GNUNET_SCHEDULER_TaskCallback trigger;
     85 
     86   /**
     87    * Closure for @e trigger.
     88    */
     89   void *trigger_cls;
     90 
     91   /**
     92    * holds the truth information
     93    */
     94   json_t *post;
     95 
     96   /**
     97    * Handle to the helper process.
     98    */
     99   struct GNUNET_OS_Process *child;
    100 
    101   /**
    102    * Handle to wait for @e child
    103    */
    104   struct GNUNET_ChildWaitHandle *cwh;
    105 
    106   /**
    107    * Our client connection, set if suspended.
    108    */
    109   struct MHD_Connection *connection;
    110 
    111   /**
    112    * Message to send.
    113    */
    114   char *msg;
    115 
    116   /**
    117    * Offset of transmission in msg.
    118    */
    119   size_t msg_off;
    120 
    121   /**
    122    * Exit code from helper.
    123    */
    124   long unsigned int exit_code;
    125 
    126   /**
    127    * How did the helper die?
    128    */
    129   enum GNUNET_OS_ProcessStatusType pst;
    130 
    131 
    132 };
    133 
    134 
    135 /**
    136  * Obtain internationalized message @a msg_id from @a ctx using
    137  * language preferences of @a conn.
    138  *
    139  * @param messages JSON object to lookup message from
    140  * @param conn connection to lookup message for
    141  * @param msg_id unique message ID
    142  * @return NULL if message was not found
    143  */
    144 static const char *
    145 get_message (const json_t *messages,
    146              struct MHD_Connection *conn,
    147              const char *msg_id)
    148 {
    149   const char *accept_lang;
    150 
    151   accept_lang = MHD_lookup_connection_value (conn,
    152                                              MHD_HEADER_KIND,
    153                                              MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
    154   if (NULL == accept_lang)
    155     accept_lang = "en_US";
    156   {
    157     const char *ret;
    158     struct GNUNET_JSON_Specification spec[] = {
    159       TALER_JSON_spec_i18n_string (msg_id,
    160                                    accept_lang,
    161                                    &ret),
    162       GNUNET_JSON_spec_end ()
    163     };
    164 
    165     if (GNUNET_OK !=
    166         GNUNET_JSON_parse (messages,
    167                            spec,
    168                            NULL, NULL))
    169     {
    170       GNUNET_break (0);
    171       GNUNET_JSON_parse_free (spec);
    172       return NULL;
    173     }
    174     GNUNET_JSON_parse_free (spec);
    175     return ret;
    176   }
    177 }
    178 
    179 
    180 /**
    181  * Validate @a data is a well-formed input into the challenge method,
    182  * i.e. @a data is a well-formed phone number for sending an SMS, or
    183  * a well-formed e-mail address for sending an e-mail. Not expected to
    184  * check that the phone number or e-mail account actually exists.
    185  *
    186  * To be possibly used before issuing a 402 payment required to the client.
    187  *
    188  * @param cls closure
    189  * @param connection HTTP client request (for queuing response)
    190  * @param mime_type mime type of @e data
    191  * @param data input to validate (i.e. is it a valid phone number, etc.)
    192  * @param data_length number of bytes in @a data
    193  * @return #GNUNET_OK if @a data is valid,
    194  *         #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection
    195  *         #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection
    196  */
    197 static enum GNUNET_GenericReturnValue
    198 post_validate (void *cls,
    199                struct MHD_Connection *connection,
    200                const char *mime_type,
    201                const char *data,
    202                size_t data_length)
    203 {
    204   struct PostContext *ctx = cls;
    205   json_t *j;
    206   json_error_t error;
    207   const char *name;
    208   const char *street;
    209   const char *city;
    210   const char *zip;
    211   const char *country;
    212   struct GNUNET_JSON_Specification spec[] = {
    213     GNUNET_JSON_spec_string ("full_name",
    214                              &name),
    215     GNUNET_JSON_spec_string ("street",
    216                              &street),
    217     GNUNET_JSON_spec_string ("city",
    218                              &city),
    219     GNUNET_JSON_spec_string ("postcode",
    220                              &zip),
    221     GNUNET_JSON_spec_string ("country",
    222                              &country),
    223     GNUNET_JSON_spec_end ()
    224   };
    225 
    226   (void) ctx;
    227   j = json_loadb (data,
    228                   data_length,
    229                   JSON_REJECT_DUPLICATES,
    230                   &error);
    231   if (NULL == j)
    232   {
    233     if (MHD_NO ==
    234         TALER_MHD_reply_with_error (connection,
    235                                     MHD_HTTP_CONFLICT,
    236                                     TALER_EC_ANASTASIS_POST_INVALID,
    237                                     "JSON malformed"))
    238       return GNUNET_SYSERR;
    239     return GNUNET_NO;
    240   }
    241 
    242   if (GNUNET_OK !=
    243       GNUNET_JSON_parse (j,
    244                          spec,
    245                          NULL, NULL))
    246   {
    247     GNUNET_break (0);
    248     json_decref (j);
    249     if (MHD_NO ==
    250         TALER_MHD_reply_with_error (connection,
    251                                     MHD_HTTP_CONFLICT,
    252                                     TALER_EC_ANASTASIS_POST_INVALID,
    253                                     "JSON lacked required address information"))
    254       return GNUNET_SYSERR;
    255     return GNUNET_NO;
    256   }
    257   json_decref (j);
    258   return GNUNET_OK;
    259 }
    260 
    261 
    262 /**
    263  * Begin issuing authentication challenge to user based on @a data.
    264  * I.e. start to send mail.
    265  *
    266  * @param cls closure
    267  * @param trigger function to call when we made progress
    268  * @param trigger_cls closure for @a trigger
    269  * @param truth_uuid Identifier of the challenge, to be (if possible) included in the
    270  *             interaction with the user
    271  * @param code secret code that the user has to provide back to satisfy the challenge in
    272  *             the main anastasis protocol
    273  * @param data input to validate (i.e. is it a valid phone number, etc.)
    274  * @param data_length number of bytes in @a data
    275  * @return state to track progress on the authorization operation, NULL on failure
    276  */
    277 static struct ANASTASIS_AUTHORIZATION_State *
    278 post_start (void *cls,
    279             GNUNET_SCHEDULER_TaskCallback trigger,
    280             void *trigger_cls,
    281             const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
    282             uint64_t code,
    283             const void *data,
    284             size_t data_length)
    285 {
    286   struct PostContext *ctx = cls;
    287   struct ANASTASIS_AUTHORIZATION_State *as;
    288   json_error_t error;
    289   enum GNUNET_DB_QueryStatus qs;
    290 
    291   /* If the user can show this challenge code, this
    292      plugin is already happy (no additional
    293      requirements), so mark this challenge as
    294      already satisfied from the start. */
    295   qs = ctx->ac->db->mark_challenge_code_satisfied (ctx->ac->db->cls,
    296                                                    truth_uuid,
    297                                                    code);
    298   if (qs <= 0)
    299   {
    300     GNUNET_break (0);
    301     return NULL;
    302   }
    303   as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State);
    304   as->trigger = trigger;
    305   as->trigger_cls = trigger_cls;
    306   as->ctx = ctx;
    307   as->truth_uuid = *truth_uuid;
    308   as->code = code;
    309   as->post = json_loadb (data,
    310                          data_length,
    311                          JSON_REJECT_DUPLICATES,
    312                          &error);
    313   if (NULL == as->post)
    314   {
    315     GNUNET_break (0);
    316     GNUNET_free (as);
    317     return NULL;
    318   }
    319   return as;
    320 }
    321 
    322 
    323 /**
    324  * Function called when our Post helper has terminated.
    325  *
    326  * @param cls our `struct ANASTASIS_AUHTORIZATION_State`
    327  * @param type type of the process
    328  * @param exit_code status code of the process
    329  */
    330 static void
    331 post_done_cb (void *cls,
    332               enum GNUNET_OS_ProcessStatusType type,
    333               long unsigned int exit_code)
    334 {
    335   struct ANASTASIS_AUTHORIZATION_State *as = cls;
    336 
    337   as->cwh = NULL;
    338   if (NULL != as->child)
    339   {
    340     GNUNET_OS_process_destroy (as->child);
    341     as->child = NULL;
    342   }
    343   as->pst = type;
    344   as->exit_code = exit_code;
    345   MHD_resume_connection (as->connection);
    346   as->trigger (as->trigger_cls);
    347 }
    348 
    349 
    350 /**
    351  * Begin issuing authentication challenge to user based on @a data.
    352  * I.e. start to send SMS or e-mail or launch video identification.
    353  *
    354  * @param as authorization state
    355  * @param connection HTTP client request (for queuing response, such as redirection to video portal)
    356  * @return state of the request
    357  */
    358 static enum ANASTASIS_AUTHORIZATION_ChallengeResult
    359 post_challenge (struct ANASTASIS_AUTHORIZATION_State *as,
    360                 struct MHD_Connection *connection)
    361 {
    362   const char *mime;
    363   const char *lang;
    364   MHD_RESULT mres;
    365   const char *name;
    366   const char *street;
    367   const char *city;
    368   const char *zip;
    369   const char *country;
    370   struct GNUNET_JSON_Specification spec[] = {
    371     GNUNET_JSON_spec_string ("full_name",
    372                              &name),
    373     GNUNET_JSON_spec_string ("street",
    374                              &street),
    375     GNUNET_JSON_spec_string ("city",
    376                              &city),
    377     GNUNET_JSON_spec_string ("postcode",
    378                              &zip),
    379     GNUNET_JSON_spec_string ("country",
    380                              &country),
    381     GNUNET_JSON_spec_end ()
    382   };
    383 
    384   mime = MHD_lookup_connection_value (connection,
    385                                       MHD_HEADER_KIND,
    386                                       MHD_HTTP_HEADER_ACCEPT);
    387   if (NULL == mime)
    388     mime = "text/plain";
    389   lang = MHD_lookup_connection_value (connection,
    390                                       MHD_HEADER_KIND,
    391                                       MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
    392   if (NULL == lang)
    393     lang = "en";
    394   if (GNUNET_OK !=
    395       GNUNET_JSON_parse (as->post,
    396                          spec,
    397                          NULL, NULL))
    398   {
    399     GNUNET_break (0);
    400     mres = TALER_MHD_reply_with_error (connection,
    401                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    402                                        TALER_EC_ANASTASIS_POST_INVALID,
    403                                        "address information incomplete");
    404     if (MHD_YES != mres)
    405       return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED;
    406     return ANASTASIS_AUTHORIZATION_CRES_FAILED;
    407   }
    408   if (NULL == as->msg)
    409   {
    410     /* First time, start child process and feed pipe */
    411     struct GNUNET_DISK_PipeHandle *p;
    412     struct GNUNET_DISK_FileHandle *pipe_stdin;
    413 
    414     p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW);
    415     if (NULL == p)
    416     {
    417       mres = TALER_MHD_reply_with_error (connection,
    418                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    419                                          TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED,
    420                                          "pipe");
    421       if (MHD_YES != mres)
    422         return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED;
    423       return ANASTASIS_AUTHORIZATION_CRES_FAILED;
    424     }
    425     as->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR,
    426                                          p,
    427                                          NULL,
    428                                          NULL,
    429                                          as->ctx->auth_command,
    430                                          as->ctx->auth_command,
    431                                          name,
    432                                          street,
    433                                          city,
    434                                          zip,
    435                                          country,
    436                                          NULL);
    437     if (NULL == as->child)
    438     {
    439       GNUNET_DISK_pipe_close (p);
    440       mres = TALER_MHD_reply_with_error (connection,
    441                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    442                                          TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED,
    443                                          "exec");
    444       if (MHD_YES != mres)
    445         return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED;
    446       return ANASTASIS_AUTHORIZATION_CRES_FAILED;
    447     }
    448     pipe_stdin = GNUNET_DISK_pipe_detach_end (p,
    449                                               GNUNET_DISK_PIPE_END_WRITE);
    450     GNUNET_assert (NULL != pipe_stdin);
    451     GNUNET_DISK_pipe_close (p);
    452     GNUNET_asprintf (&as->msg,
    453                      get_message (as->ctx->messages,
    454                                   connection,
    455                                   "body"),
    456                      ANASTASIS_pin2s (as->code),
    457                      ANASTASIS_CRYPTO_uuid2s (&as->truth_uuid));
    458     {
    459       const char *off = as->msg;
    460       size_t left = strlen (off);
    461 
    462       while (0 != left)
    463       {
    464         ssize_t ret;
    465 
    466         ret = GNUNET_DISK_file_write (pipe_stdin,
    467                                       off,
    468                                       left);
    469         if (ret <= 0)
    470         {
    471           mres = TALER_MHD_reply_with_error (connection,
    472                                              MHD_HTTP_INTERNAL_SERVER_ERROR,
    473                                              TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED,
    474                                              "write");
    475           if (MHD_YES != mres)
    476             return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED;
    477           return ANASTASIS_AUTHORIZATION_CRES_FAILED;
    478         }
    479         as->msg_off += ret;
    480         off += ret;
    481         left -= ret;
    482       }
    483       GNUNET_DISK_file_close (pipe_stdin);
    484     }
    485     as->cwh = GNUNET_wait_child (as->child,
    486                                  &post_done_cb,
    487                                  as);
    488     as->connection = connection;
    489     MHD_suspend_connection (connection);
    490     return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED;
    491   }
    492   if (NULL != as->cwh)
    493   {
    494     /* Spurious call, why are we here? */
    495     GNUNET_break (0);
    496     MHD_suspend_connection (connection);
    497     return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED;
    498   }
    499   if ( (GNUNET_OS_PROCESS_EXITED != as->pst) ||
    500        (0 != as->exit_code) )
    501   {
    502     char es[32];
    503 
    504     GNUNET_snprintf (es,
    505                      sizeof (es),
    506                      "%u/%d",
    507                      (unsigned int) as->exit_code,
    508                      as->pst);
    509     mres = TALER_MHD_reply_with_error (connection,
    510                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    511                                        TALER_EC_ANASTASIS_POST_HELPER_COMMAND_FAILED,
    512                                        es);
    513     if (MHD_YES != mres)
    514       return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED;
    515     return ANASTASIS_AUTHORIZATION_CRES_FAILED;
    516   }
    517 
    518   /* Build HTTP response */
    519   {
    520     struct MHD_Response *resp;
    521 
    522     if (0.0 < TALER_pattern_matches (mime,
    523                                      "application/json"))
    524     {
    525       resp = TALER_MHD_MAKE_JSON_PACK (
    526         GNUNET_JSON_pack_string ("challenge_type",
    527                                  "TAN_SENT"),
    528         GNUNET_JSON_pack_string ("tan_address_hint",
    529                                  zip));
    530     }
    531     else
    532     {
    533       size_t reply_len;
    534       char *reply;
    535 
    536       reply_len = GNUNET_asprintf (&reply,
    537                                    get_message (as->ctx->messages,
    538                                                 connection,
    539                                                 "instructions"),
    540                                    zip);
    541       resp = MHD_create_response_from_buffer (reply_len,
    542                                               reply,
    543                                               MHD_RESPMEM_MUST_COPY);
    544       GNUNET_free (reply);
    545       TALER_MHD_add_global_headers (resp,
    546                                     false);
    547     }
    548     mres = MHD_queue_response (connection,
    549                                MHD_HTTP_OK,
    550                                resp);
    551     MHD_destroy_response (resp);
    552     if (MHD_YES != mres)
    553       return ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED;
    554     return ANASTASIS_AUTHORIZATION_CRES_SUCCESS;
    555   }
    556 }
    557 
    558 
    559 /**
    560  * Free internal state associated with @a as.
    561  *
    562  * @param as state to clean up
    563  */
    564 static void
    565 post_cleanup (struct ANASTASIS_AUTHORIZATION_State *as)
    566 {
    567   if (NULL != as->cwh)
    568   {
    569     GNUNET_wait_child_cancel (as->cwh);
    570     as->cwh = NULL;
    571   }
    572   if (NULL != as->child)
    573   {
    574     (void) GNUNET_OS_process_kill (as->child,
    575                                    SIGKILL);
    576     GNUNET_break (GNUNET_OK ==
    577                   GNUNET_OS_process_wait (as->child));
    578     as->child = NULL;
    579   }
    580   GNUNET_free (as->msg);
    581   json_decref (as->post);
    582   GNUNET_free (as);
    583 }
    584 
    585 
    586 /**
    587  * Initialize post based authorization plugin
    588  *
    589  * @param cls a configuration instance
    590  * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin`
    591  */
    592 void *
    593 libanastasis_plugin_authorization_post_init (void *cls);
    594 
    595 /* declaration to fix compiler warning */
    596 void *
    597 libanastasis_plugin_authorization_post_init (void *cls)
    598 {
    599   const struct ANASTASIS_AuthorizationContext *ac = cls;
    600   struct ANASTASIS_AuthorizationPlugin *plugin;
    601   const struct GNUNET_CONFIGURATION_Handle *cfg = ac->cfg;
    602   struct PostContext *ctx;
    603 
    604   ctx = GNUNET_new (struct PostContext);
    605   ctx->ac = ac;
    606   {
    607     char *fn;
    608     json_error_t err;
    609     char *tmp;
    610 
    611     tmp = GNUNET_OS_installation_get_path (ANASTASIS_project_data (),
    612                                            GNUNET_OS_IPK_DATADIR);
    613     GNUNET_asprintf (&fn,
    614                      "%sauthorization-post-messages.json",
    615                      tmp);
    616     GNUNET_free (tmp);
    617     ctx->messages = json_load_file (fn,
    618                                     JSON_REJECT_DUPLICATES,
    619                                     &err);
    620     if (NULL == ctx->messages)
    621     {
    622       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    623                   "Failed to load messages from `%s': %s at %d:%d\n",
    624                   fn,
    625                   err.text,
    626                   err.line,
    627                   err.column);
    628       GNUNET_free (fn);
    629       GNUNET_free (ctx);
    630       return NULL;
    631     }
    632     GNUNET_free (fn);
    633   }
    634   plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin);
    635   plugin->retry_counter = INITIAL_RETRY_COUNTER;
    636   plugin->code_validity_period = GNUNET_TIME_UNIT_MONTHS;
    637   plugin->code_rotation_period = GNUNET_TIME_UNIT_WEEKS;
    638   plugin->code_retransmission_frequency
    639     = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_DAYS,
    640                                      2);
    641   plugin->cls = ctx;
    642   plugin->validate = &post_validate;
    643   plugin->start = &post_start;
    644   plugin->challenge = &post_challenge;
    645   plugin->cleanup = &post_cleanup;
    646 
    647   if (GNUNET_OK !=
    648       GNUNET_CONFIGURATION_get_value_string (cfg,
    649                                              "authorization-post",
    650                                              "COMMAND",
    651                                              &ctx->auth_command))
    652   {
    653     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    654                                "authorization-post",
    655                                "COMMAND");
    656     json_decref (ctx->messages);
    657     GNUNET_free (ctx);
    658     GNUNET_free (plugin);
    659     return NULL;
    660   }
    661   return plugin;
    662 }
    663 
    664 
    665 /**
    666  * Unload authorization plugin
    667  *
    668  * @param cls a `struct ANASTASIS_AuthorizationPlugin`
    669  * @return NULL (always)
    670  */
    671 void *
    672 libanastasis_plugin_authorization_post_done (void *cls);
    673 
    674 /* declaration to fix compiler warning */
    675 void *
    676 libanastasis_plugin_authorization_post_done (void *cls)
    677 {
    678   struct ANASTASIS_AuthorizationPlugin *plugin = cls;
    679   struct PostContext *ctx = plugin->cls;
    680 
    681   GNUNET_free (ctx->auth_command);
    682   json_decref (ctx->messages);
    683   GNUNET_free (ctx);
    684   GNUNET_free (plugin);
    685   return NULL;
    686 }