From 7e669bcf6b6336ec429da949bcb4aa456971dba2 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Fri, 30 Jul 2021 10:38:27 +0200 Subject: folding history in preparation of GNU Anastasis v0.0.0 release --- src/authorization/Makefile.am | 98 +++ src/authorization/anastasis_authorization_plugin.c | 239 ++++++++ .../anastasis_authorization_plugin_email.c | 616 +++++++++++++++++++ .../anastasis_authorization_plugin_file.c | 302 ++++++++++ .../anastasis_authorization_plugin_post.c | 655 +++++++++++++++++++++ .../anastasis_authorization_plugin_sms.c | 607 +++++++++++++++++++ .../authorization-email-messages.json | 10 + src/authorization/authorization-post-messages.json | 7 + src/authorization/authorization-sms-messages.json | 6 + 9 files changed, 2540 insertions(+) create mode 100644 src/authorization/Makefile.am create mode 100644 src/authorization/anastasis_authorization_plugin.c create mode 100644 src/authorization/anastasis_authorization_plugin_email.c create mode 100644 src/authorization/anastasis_authorization_plugin_file.c create mode 100644 src/authorization/anastasis_authorization_plugin_post.c create mode 100644 src/authorization/anastasis_authorization_plugin_sms.c create mode 100644 src/authorization/authorization-email-messages.json create mode 100644 src/authorization/authorization-post-messages.json create mode 100644 src/authorization/authorization-sms-messages.json (limited to 'src/authorization') diff --git a/src/authorization/Makefile.am b/src/authorization/Makefile.am new file mode 100644 index 0000000..8ea7e86 --- /dev/null +++ b/src/authorization/Makefile.am @@ -0,0 +1,98 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include + +pkgcfgdir = $(prefix)/share/anastasis/config.d/ +plugindir = $(libdir)/anastasis +pkgdatadir= $(prefix)/share/anastasis/ + +pkgdata_DATA = \ + authorization-email-messages.json \ + authorization-post-messages.json \ + authorization-sms-messages.json + +EXTRA_DIST = $(pkgdata_DATA) + + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +lib_LTLIBRARIES = \ + libanastasisauthorization.la + +libanastasisauthorization_la_SOURCES = \ + anastasis_authorization_plugin.c +libanastasisauthorization_la_LIBADD = \ + $(LTLIBINTL) +libanastasisauthorization_la_LDFLAGS = \ + -ltalerutil \ + -lgnunetutil \ + -lmicrohttpd \ + -lltdl \ + $(XLIB) + +plugin_LTLIBRARIES = \ + libanastasis_plugin_authorization_email.la \ + libanastasis_plugin_authorization_file.la \ + libanastasis_plugin_authorization_post.la \ + libanastasis_plugin_authorization_sms.la +libanastasis_plugin_authorization_file_la_SOURCES = \ + anastasis_authorization_plugin_file.c +libanastasis_plugin_authorization_file_la_LIBADD = \ + $(LTLIBINTL) +libanastasis_plugin_authorization_file_la_LDFLAGS = \ + $(ANASTASIS_PLUGIN_LDFLAGS) \ + -ltalerjson \ + -ltalermhd \ + -ltalerutil \ + -lgnunetjson \ + -lgnunetutil \ + -lmicrohttpd \ + -ljansson \ + $(XLIB) + +libanastasis_plugin_authorization_email_la_SOURCES = \ + anastasis_authorization_plugin_email.c +libanastasis_plugin_authorization_email_la_LIBADD = \ + $(LTLIBINTL) +libanastasis_plugin_authorization_email_la_LDFLAGS = \ + $(ANASTASIS_PLUGIN_LDFLAGS) \ + -ltalerjson \ + -ltalermhd \ + -ltalerutil \ + -lgnunetjson \ + -lgnunetutil \ + -lmicrohttpd \ + -ljansson \ + $(XLIB) + +libanastasis_plugin_authorization_post_la_SOURCES = \ + anastasis_authorization_plugin_post.c +libanastasis_plugin_authorization_post_la_LIBADD = \ + $(LTLIBINTL) +libanastasis_plugin_authorization_post_la_LDFLAGS = \ + $(ANASTASIS_PLUGIN_LDFLAGS) \ + -ltalerjson \ + -ltalermhd \ + -ltalerutil \ + -lgnunetjson \ + -lgnunetutil \ + -lmicrohttpd \ + -ljansson \ + $(XLIB) + +libanastasis_plugin_authorization_sms_la_SOURCES = \ + anastasis_authorization_plugin_sms.c +libanastasis_plugin_authorization_sms_la_LIBADD = \ + $(LTLIBINTL) +libanastasis_plugin_authorization_sms_la_LDFLAGS = \ + $(ANASTASIS_PLUGIN_LDFLAGS) \ + -ltalerjson \ + -ltalermhd \ + -ltalerutil \ + -lgnunetjson \ + -lgnunetutil \ + -ljansson \ + -lmicrohttpd \ + $(XLIB) diff --git a/src/authorization/anastasis_authorization_plugin.c b/src/authorization/anastasis_authorization_plugin.c new file mode 100644 index 0000000..7874594 --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin.c @@ -0,0 +1,239 @@ +/* + This file is part of TALER + Copyright (C) 2015, 2016, 2021 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file anastasis_authorization_plugin.c + * @brief Logic to load database plugin + * @author Christian Grothoff + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include + + +/** + * Head of linked list for all loaded plugins + */ +static struct AuthPlugin *ap_head; + +/** + * Tail ofinked list for all loaded plugins + */ +static struct AuthPlugin *ap_tail; + + +/** + * Authentication plugin which is used to verify code based authentication + * like SMS, E-Mail. + */ +struct AuthPlugin +{ + /** + * Kept in a DLL. + */ + struct AuthPlugin *next; + + /** + * Kept in a DLL. + */ + struct AuthPlugin *prev; + + /** + * Actual plugin handle. + */ + struct ANASTASIS_AuthorizationPlugin *authorization; + + /** + * I.e. "sms", "phone". + */ + char *name; + + /** + * Name of the shared object providing the plugin logic. + */ + char *lib_name; + + /** + * Cost of using this plugin. + */ + struct TALER_Amount cost; +}; + + +struct ANASTASIS_AuthorizationPlugin * +ANASTASIS_authorization_plugin_load ( + const char *method, + const struct GNUNET_CONFIGURATION_Handle *AH_cfg, + struct TALER_Amount *cost) +{ + struct ANASTASIS_AuthorizationPlugin *authorization; + char *lib_name; + char *sec_name; + struct AuthPlugin *ap; + char *currency; + + for (ap = ap_head; NULL != ap; ap = ap->next) + if (0 == strcmp (method, + ap->name)) + { + *cost = ap->cost; + return ap->authorization; + } + if (GNUNET_OK != + TALER_config_get_currency (AH_cfg, + ¤cy)) + return NULL; + ap = GNUNET_new (struct AuthPlugin); + GNUNET_asprintf (&sec_name, + "authorization-%s", + method); + if (GNUNET_OK != + TALER_config_get_amount (AH_cfg, + sec_name, + "COST", + &ap->cost)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + sec_name, + "COST"); + GNUNET_free (sec_name); + GNUNET_free (currency); + GNUNET_free (ap); + return NULL; + } + + if (0 != + strcasecmp (currency, + ap->cost.currency)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + sec_name, + "COST", + "currency mismatch"); + GNUNET_free (currency); + GNUNET_free (sec_name); + GNUNET_free (ap); + return NULL; + } + GNUNET_free (currency); + GNUNET_free (sec_name); + GNUNET_asprintf (&lib_name, + "libanastasis_plugin_authorization_%s", + method); + authorization = GNUNET_PLUGIN_load (lib_name, + (void *) AH_cfg); + if (NULL == authorization) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Authentication method `%s' not supported\n", + method); + GNUNET_free (lib_name); + GNUNET_free (ap); + return NULL; + } + ap->name = GNUNET_strdup (method); + ap->lib_name = lib_name; + ap->authorization = authorization; + GNUNET_CONTAINER_DLL_insert (ap_head, + ap_tail, + ap); + *cost = ap->cost; + return authorization; +} + + +void +ANASTASIS_authorization_plugin_shutdown (void) +{ + struct AuthPlugin *ap; + + while (NULL != (ap = ap_head)) + { + GNUNET_CONTAINER_DLL_remove (ap_head, + ap_tail, + ap); + GNUNET_PLUGIN_unload (ap->lib_name, + ap->authorization); + GNUNET_free (ap->lib_name); + GNUNET_free (ap->name); + GNUNET_free (ap); + } +} + + +/** + * Libtool search path before we started. + */ +static char *old_dlsearchpath; + + +/** + * Setup libtool paths. + */ +void __attribute__ ((constructor)) +anastasis_authorization_plugin_init (void) +{ + int err; + const char *opath; + char *path; + char *cpath; + + err = lt_dlinit (); + if (err > 0) + { + fprintf (stderr, + _ ("Initialization of plugin mechanism failed: %s!\n"), + lt_dlerror ()); + return; + } + opath = lt_dlgetsearchpath (); + if (NULL != opath) + old_dlsearchpath = GNUNET_strdup (opath); + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_LIBDIR); + if (NULL != path) + { + if (NULL != opath) + { + GNUNET_asprintf (&cpath, "%s:%s", opath, path); + lt_dlsetsearchpath (cpath); + GNUNET_free (path); + GNUNET_free (cpath); + } + else + { + lt_dlsetsearchpath (path); + GNUNET_free (path); + } + } +} + + +/** + * Shutdown libtool. + */ +void __attribute__ ((destructor)) +anastasis_authorization_plugin_fini (void) +{ + lt_dlsetsearchpath (old_dlsearchpath); + if (NULL != old_dlsearchpath) + { + GNUNET_free (old_dlsearchpath); + } + lt_dlexit (); +} + + +/* end of anastasis_authorization_plugin.c */ diff --git a/src/authorization/anastasis_authorization_plugin_email.c b/src/authorization/anastasis_authorization_plugin_email.c new file mode 100644 index 0000000..33c400b --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_email.c @@ -0,0 +1,616 @@ +/* + This file is part of Anastasis + Copyright (C) 2019-2021 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file include/anastasis_authorization_plugin_email.c + * @brief authorization plugin email based + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include +#include +#include +#include "anastasis_util_lib.h" + + +/** + * Saves the State of a authorization plugin. + */ +struct Email_Context +{ + + /** + * Command which is executed to run the plugin (some bash script or a + * command line argument) + */ + char *auth_command; + + /** + * Regex for email address validation. + */ + regex_t regex; + + /** + * Messages of the plugin, read from a resource file. + */ + json_t *messages; + +}; + + +/** + * Saves the state of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * Public key of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Code which is sent to the user. + */ + uint64_t code; + + /** + * Our plugin context. + */ + struct Email_Context *ctx; + + /** + * Function to call when we made progress. + */ + GNUNET_SCHEDULER_TaskCallback trigger; + + /** + * Closure for @e trigger. + */ + void *trigger_cls; + + /** + * holds the truth information + */ + char *email; + + /** + * Handle to the helper process. + */ + struct GNUNET_OS_Process *child; + + /** + * Handle to wait for @e child + */ + struct GNUNET_ChildWaitHandle *cwh; + + /** + * Our client connection, set if suspended. + */ + struct MHD_Connection *connection; + + /** + * Message to send. + */ + char *msg; + + /** + * Offset of transmission in msg. + */ + size_t msg_off; + + /** + * Exit code from helper. + */ + long unsigned int exit_code; + + /** + * How did the helper die? + */ + enum GNUNET_OS_ProcessStatusType pst; + +}; + + +/** + * Obtain internationalized message @a msg_id from @a ctx using + * language preferences of @a conn. + * + * @param messages JSON object to lookup message from + * @param conn connection to lookup message for + * @param msg_id unique message ID + * @return NULL if message was not found + */ +static const char * +get_message (const json_t *messages, + struct MHD_Connection *conn, + const char *msg_id) +{ + const char *accept_lang; + + accept_lang = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == accept_lang) + accept_lang = "en_US"; + { + const char *ret; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_i18n_string (msg_id, + accept_lang, + &ret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (messages, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + return ret; + } +} + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed phone number for sending an SMS, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the phone number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +email_validate (void *cls, + struct MHD_Connection *connection, + const char *mime_type, + const char *data, + size_t data_length) +{ + struct Email_Context *ctx = cls; + int regex_result; + char *phone_number; + + phone_number = GNUNET_strndup (data, + data_length); + regex_result = regexec (&ctx->regex, + phone_number, + 0, + NULL, + 0); + GNUNET_free (phone_number); + if (0 != regex_result) + { + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_EMAIL_INVALID, + NULL)) + return GNUNET_SYSERR; + return GNUNET_NO; + } + return GNUNET_OK; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param cls closure + * @param trigger function to call when we made progress + * @param trigger_cls closure for @a trigger + * @param truth_uuid Identifier of the challenge, to be (if possible) included in the + * interaction with the user + * @param code secret code that the user has to provide back to satisfy the challenge in + * the main anastasis protocol + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @return state to track progress on the authorization operation, NULL on failure + */ +static struct ANASTASIS_AUTHORIZATION_State * +email_start (void *cls, + GNUNET_SCHEDULER_TaskCallback trigger, + void *trigger_cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code, + const void *data, + size_t data_length) +{ + struct Email_Context *ctx = cls; + struct ANASTASIS_AUTHORIZATION_State *as; + + as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); + as->trigger = trigger; + as->trigger_cls = trigger_cls; + as->ctx = ctx; + as->truth_uuid = *truth_uuid; + as->code = code; + as->email = GNUNET_strndup (data, + data_length); + return as; +} + + +/** + * Function called when our Email helper has terminated. + * + * @param cls our `struct ANASTASIS_AUHTORIZATION_State` + * @param type type of the process + * @param exit_code status code of the process + */ +static void +email_done_cb (void *cls, + enum GNUNET_OS_ProcessStatusType type, + long unsigned int exit_code) +{ + struct ANASTASIS_AUTHORIZATION_State *as = cls; + + as->child = NULL; + as->cwh = NULL; + as->pst = type; + as->exit_code = exit_code; + MHD_resume_connection (as->connection); + as->trigger (as->trigger_cls); +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param as authorization state + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +email_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection) +{ + MHD_RESULT mres; + const char *mime; + const char *lang; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + if (NULL == as->msg) + { + /* First time, start child process and feed pipe */ + struct GNUNET_DISK_PipeHandle *p; + struct GNUNET_DISK_FileHandle *pipe_stdin; + + p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); + if (NULL == p) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, + "pipe"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR, + p, + NULL, + NULL, + as->ctx->auth_command, + as->ctx->auth_command, + as->email, + NULL); + if (NULL == as->child) + { + GNUNET_DISK_pipe_close (p); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, + "exec"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + pipe_stdin = GNUNET_DISK_pipe_detach_end (p, + GNUNET_DISK_PIPE_END_WRITE); + GNUNET_assert (NULL != pipe_stdin); + GNUNET_DISK_pipe_close (p); + { + char *tpk; + + tpk = GNUNET_STRINGS_data_to_string_alloc ( + &as->truth_uuid, + sizeof (as->truth_uuid)); + GNUNET_asprintf (&as->msg, + get_message (as->ctx->messages, + connection, + "body"), + (unsigned long long) as->code, + tpk); + GNUNET_free (tpk); + } + + { + const char *off = as->msg; + size_t left = strlen (off); + + while (0 != left) + { + ssize_t ret; + + if (0 == left) + break; + ret = GNUNET_DISK_file_write (pipe_stdin, + off, + left); + if (ret <= 0) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, + "write"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->msg_off += ret; + off += ret; + left -= ret; + } + GNUNET_DISK_file_close (pipe_stdin); + } + as->cwh = GNUNET_wait_child (as->child, + &email_done_cb, + as); + as->connection = connection; + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if (NULL != as->cwh) + { + /* Spurious call, why are we here? */ + GNUNET_break (0); + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || + (0 != as->exit_code) ) + { + char es[32]; + + GNUNET_snprintf (es, + sizeof (es), + "%u/%d", + (unsigned int) as->exit_code, + as->pst); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_EMAIL_HELPER_COMMAND_FAILED, + es); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + + /* Build HTTP response */ + { + struct MHD_Response *resp; + const char *at; + size_t len; + + at = strchr (as->email, '@'); + if (NULL == at) + len = 0; + else + len = at - as->email; + + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + json_t *body; + char *user; + + user = GNUNET_strndup (as->email, + len); + body = json_pack ("{s:I, s:s, s:s}", + "code", + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, + "hint", + TALER_ErrorCode_get_hint ( + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED), + "detail", + user); + GNUNET_free (user); + GNUNET_break (NULL != body); + resp = TALER_MHD_make_json (body); + } + else + { + size_t reply_len; + char *reply; + + reply_len = GNUNET_asprintf (&reply, + get_message (as->ctx->messages, + connection, + "instructions"), + (unsigned int) len, + as->email); + resp = MHD_create_response_from_buffer (reply_len, + reply, + MHD_RESPMEM_MUST_COPY); + GNUNET_free (reply); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + } + mres = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; + } +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +email_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + if (NULL != as->cwh) + { + GNUNET_wait_child_cancel (as->cwh); + as->cwh = NULL; + } + if (NULL != as->child) + { + (void) GNUNET_OS_process_kill (as->child, + SIGKILL); + GNUNET_break (GNUNET_OK == + GNUNET_OS_process_wait (as->child)); + as->child = NULL; + } + GNUNET_free (as->msg); + GNUNET_free (as->email); + GNUNET_free (as); +} + + +/** + * Initialize email based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_email_init (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin; + struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct Email_Context *ctx; + + ctx = GNUNET_new (struct Email_Context); + { + char *fn; + json_error_t err; + + GNUNET_asprintf (&fn, + "%sauthorization-email-messages.json", + GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR)); + ctx->messages = json_load_file (fn, + JSON_REJECT_DUPLICATES, + &err); + if (NULL == ctx->messages) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load messages from `%s': %s at %d:%d\n", + fn, + err.text, + err.line, + err.column); + GNUNET_free (fn); + GNUNET_free (ctx); + return NULL; + } + GNUNET_free (fn); + } + { + int regex_result; + const char *regexp = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}"; + + regex_result = regcomp (&ctx->regex, + regexp, + REG_EXTENDED); + if (0 < regex_result) + { + GNUNET_break (0); + json_decref (ctx->messages); + GNUNET_free (ctx); + return NULL; + } + } + + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS; + plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS; + plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; + plugin->cls = ctx; + plugin->validate = &email_validate; + plugin->start = &email_start; + plugin->process = &email_process; + plugin->cleanup = &email_cleanup; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "authorization-email", + "COMMAND", + &ctx->auth_command)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-email", + "COMMAND"); + regfree (&ctx->regex); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; + } + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_email_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + struct Email_Context *ctx = plugin->cls; + + GNUNET_free (ctx->auth_command); + regfree (&ctx->regex); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; +} diff --git a/src/authorization/anastasis_authorization_plugin_file.c b/src/authorization/anastasis_authorization_plugin_file.c new file mode 100644 index 0000000..210ade7 --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_file.c @@ -0,0 +1,302 @@ +/* + This file is part of Anastasis + Copyright (C) 2019 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file include/anastasis_authorization_plugin_file.c + * @brief authorization plugin file based for testing + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include + + +/** + * Saves the state of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * UUID of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Code which is sent to the user (here saved into a file) + */ + uint64_t code; + + /** + * holds the truth information + */ + char *filename; + + /** + * closure + */ + void *cls; +}; + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed phone number for sending an SMS, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the phone number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +file_validate (void *cls, + struct MHD_Connection *connection, + const char *truth_mime, + const char *data, + size_t data_length) +{ + char *filename; + bool flag; + + if (NULL == data) + return GNUNET_NO; + filename = GNUNET_STRINGS_data_to_string_alloc (data, + data_length); + flag = false; + for (size_t i = 0; icls = cls; + as->truth_uuid = *truth_uuid; + as->code = code; + as->filename = GNUNET_strndup (data, + data_length); + return as; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param as authorization state + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +file_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection) +{ + const char *mime; + const char *lang; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + { + FILE *f = fopen (as->filename, "w"); + + if (NULL == f) + { + struct MHD_Response *resp; + MHD_RESULT mres; + + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + as->filename); + resp = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "open"); + mres = MHD_queue_response (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + + /* print challenge code to file */ + if (0 >= fprintf (f, + "%lu", + as->code)) + { + struct MHD_Response *resp; + MHD_RESULT mres; + + GNUNET_break (0 == fclose (f)); + resp = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "write"); + mres = MHD_queue_response (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + GNUNET_break (0 == fclose (f)); + } + + /* Build HTTP response */ + { + struct MHD_Response *resp; + + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + json_t *body; + + body = json_pack ("{s:s}", + "filename", + as->filename); + GNUNET_break (NULL != body); + resp = TALER_MHD_make_json (body); + } + else + { + size_t response_size; + char *response; + + response_size = GNUNET_asprintf (&response, + _ ("Challenge written to file")); + resp = MHD_create_response_from_buffer (response_size, + response, + MHD_RESPMEM_MUST_COPY); + GNUNET_free (response); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + } + + { + MHD_RESULT mres; + + mres = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; + } + } +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +file_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + GNUNET_free (as->filename); + GNUNET_free (as); +} + + +/** + * Initialize File based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_file_init (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin; + + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->code_validity_period = GNUNET_TIME_UNIT_MINUTES; + plugin->code_rotation_period = GNUNET_TIME_UNIT_MINUTES; + plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; + plugin->validate = &file_validate; + plugin->start = &file_start; + plugin->process = &file_process; + plugin->cleanup = &file_cleanup; + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_file_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + + GNUNET_free (plugin); + return NULL; +} diff --git a/src/authorization/anastasis_authorization_plugin_post.c b/src/authorization/anastasis_authorization_plugin_post.c new file mode 100644 index 0000000..1f20ff3 --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_post.c @@ -0,0 +1,655 @@ +/* + This file is part of Anastasis + Copyright (C) 2021 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file include/anastasis_authorization_plugin_post.c + * @brief authorization plugin post based + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include +#include +#include +#include "anastasis_util_lib.h" + + +/** + * Saves the State of a authorization plugin. + */ +struct PostContext +{ + + /** + * Command which is executed to run the plugin (some bash script or a + * command line argument) + */ + char *auth_command; + + /** + * Messages of the plugin, read from a resource file. + */ + json_t *messages; +}; + + +/** + * Saves the state of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * Public key of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Code which is sent to the user. + */ + uint64_t code; + + /** + * Our plugin context. + */ + struct PostContext *ctx; + + /** + * Function to call when we made progress. + */ + GNUNET_SCHEDULER_TaskCallback trigger; + + /** + * Closure for @e trigger. + */ + void *trigger_cls; + + /** + * holds the truth information + */ + json_t *post; + + /** + * Handle to the helper process. + */ + struct GNUNET_OS_Process *child; + + /** + * Handle to wait for @e child + */ + struct GNUNET_ChildWaitHandle *cwh; + + /** + * Our client connection, set if suspended. + */ + struct MHD_Connection *connection; + + /** + * Message to send. + */ + char *msg; + + /** + * Offset of transmission in msg. + */ + size_t msg_off; + + /** + * Exit code from helper. + */ + long unsigned int exit_code; + + /** + * How did the helper die? + */ + enum GNUNET_OS_ProcessStatusType pst; + + +}; + + +/** + * Obtain internationalized message @a msg_id from @a ctx using + * language preferences of @a conn. + * + * @param messages JSON object to lookup message from + * @param conn connection to lookup message for + * @param msg_id unique message ID + * @return NULL if message was not found + */ +static const char * +get_message (const json_t *messages, + struct MHD_Connection *conn, + const char *msg_id) +{ + const char *accept_lang; + + accept_lang = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == accept_lang) + accept_lang = "en_US"; + { + const char *ret; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_i18n_string (msg_id, + accept_lang, + &ret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (messages, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + return ret; + } +} + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed phone number for sending an SMS, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the phone number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +post_validate (void *cls, + struct MHD_Connection *connection, + const char *mime_type, + const char *data, + size_t data_length) +{ + struct PostContext *ctx = cls; + json_t *j; + json_error_t error; + const char *name; + const char *street; + const char *city; + const char *zip; + const char *country; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("full_name", + &name), + GNUNET_JSON_spec_string ("street", + &street), + GNUNET_JSON_spec_string ("city", + &city), + GNUNET_JSON_spec_string ("postcode", + &zip), + GNUNET_JSON_spec_string ("country", + &country), + GNUNET_JSON_spec_end () + }; + + (void) ctx; + j = json_loadb (data, + data_length, + JSON_REJECT_DUPLICATES, + &error); + if (NULL == j) + { + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_POST_INVALID, + "JSON malformed")) + return GNUNET_SYSERR; + return GNUNET_NO; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (j); + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_POST_INVALID, + "JSON lacked required address information")) + return GNUNET_SYSERR; + return GNUNET_NO; + } + json_decref (j); + return GNUNET_OK; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send mail. + * + * @param cls closure + * @param trigger function to call when we made progress + * @param trigger_cls closure for @a trigger + * @param truth_uuid Identifier of the challenge, to be (if possible) included in the + * interaction with the user + * @param code secret code that the user has to provide back to satisfy the challenge in + * the main anastasis protocol + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @return state to track progress on the authorization operation, NULL on failure + */ +static struct ANASTASIS_AUTHORIZATION_State * +post_start (void *cls, + GNUNET_SCHEDULER_TaskCallback trigger, + void *trigger_cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code, + const void *data, + size_t data_length) +{ + struct PostContext *ctx = cls; + struct ANASTASIS_AUTHORIZATION_State *as; + json_error_t error; + + as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); + as->trigger = trigger; + as->trigger_cls = trigger_cls; + as->ctx = ctx; + as->truth_uuid = *truth_uuid; + as->code = code; + as->post = json_loadb (data, + data_length, + JSON_REJECT_DUPLICATES, + &error); + if (NULL == as->post) + { + GNUNET_break (0); + GNUNET_free (as); + return NULL; + } + return as; +} + + +/** + * Function called when our Post helper has terminated. + * + * @param cls our `struct ANASTASIS_AUHTORIZATION_State` + * @param type type of the process + * @param exit_code status code of the process + */ +static void +post_done_cb (void *cls, + enum GNUNET_OS_ProcessStatusType type, + long unsigned int exit_code) +{ + struct ANASTASIS_AUTHORIZATION_State *as = cls; + + as->child = NULL; + as->cwh = NULL; + as->pst = type; + as->exit_code = exit_code; + MHD_resume_connection (as->connection); + as->trigger (as->trigger_cls); +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param as authorization state + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +post_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection) +{ + const char *mime; + const char *lang; + MHD_RESULT mres; + const char *name; + const char *street; + const char *city; + const char *zip; + const char *country; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("full_name", + &name), + GNUNET_JSON_spec_string ("street", + &street), + GNUNET_JSON_spec_string ("city", + &city), + GNUNET_JSON_spec_string ("postcode", + &zip), + GNUNET_JSON_spec_string ("country", + &country), + GNUNET_JSON_spec_end () + }; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + if (GNUNET_OK != + GNUNET_JSON_parse (as->post, + spec, + NULL, NULL)) + { + GNUNET_break (0); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_INVALID, + "address information incomplete"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + if (NULL == as->msg) + { + /* First time, start child process and feed pipe */ + struct GNUNET_DISK_PipeHandle *p; + struct GNUNET_DISK_FileHandle *pipe_stdin; + + p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); + if (NULL == p) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED, + "pipe"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR, + p, + NULL, + NULL, + as->ctx->auth_command, + as->ctx->auth_command, + name, + street, + city, + zip, + country, + NULL); + if (NULL == as->child) + { + GNUNET_DISK_pipe_close (p); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED, + "exec"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + pipe_stdin = GNUNET_DISK_pipe_detach_end (p, + GNUNET_DISK_PIPE_END_WRITE); + GNUNET_assert (NULL != pipe_stdin); + GNUNET_DISK_pipe_close (p); + { + char *tpk; + + tpk = GNUNET_STRINGS_data_to_string_alloc ( + &as->truth_uuid, + sizeof (as->truth_uuid)); + GNUNET_asprintf (&as->msg, + get_message (as->ctx->messages, + connection, + "body"), + (unsigned long long) as->code, + tpk); + GNUNET_free (tpk); + } + + { + const char *off = as->msg; + size_t left = strlen (off); + + while (0 != left) + { + ssize_t ret; + + if (0 == left) + break; + ret = GNUNET_DISK_file_write (pipe_stdin, + off, + left); + if (ret <= 0) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED, + "write"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->msg_off += ret; + off += ret; + left -= ret; + } + GNUNET_DISK_file_close (pipe_stdin); + } + as->cwh = GNUNET_wait_child (as->child, + &post_done_cb, + as); + as->connection = connection; + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if (NULL != as->cwh) + { + /* Spurious call, why are we here? */ + GNUNET_break (0); + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || + (0 != as->exit_code) ) + { + char es[32]; + + GNUNET_snprintf (es, + sizeof (es), + "%u/%d", + (unsigned int) as->exit_code, + as->pst); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_COMMAND_FAILED, + es); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + + /* Build HTTP response */ + { + struct MHD_Response *resp; + + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + json_t *body; + + body = json_pack ("{s:I, s:s, s:s}", + "code", + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, + "hint", + TALER_ErrorCode_get_hint ( + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED), + "detail", + zip); + GNUNET_break (NULL != body); + resp = TALER_MHD_make_json (body); + } + else + { + size_t reply_len; + char *reply; + + reply_len = GNUNET_asprintf (&reply, + get_message (as->ctx->messages, + connection, + "instructions"), + zip); + resp = MHD_create_response_from_buffer (reply_len, + reply, + MHD_RESPMEM_MUST_COPY); + GNUNET_free (reply); + TALER_MHD_add_global_headers (resp); + } + mres = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; + } +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +post_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + if (NULL != as->cwh) + { + GNUNET_wait_child_cancel (as->cwh); + as->cwh = NULL; + } + if (NULL != as->child) + { + (void) GNUNET_OS_process_kill (as->child, + SIGKILL); + GNUNET_break (GNUNET_OK == + GNUNET_OS_process_wait (as->child)); + as->child = NULL; + } + GNUNET_free (as->msg); + json_decref (as->post); + GNUNET_free (as); +} + + +/** + * Initialize post based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_post_init (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin; + struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct PostContext *ctx; + + ctx = GNUNET_new (struct PostContext); + { + char *fn; + json_error_t err; + + GNUNET_asprintf (&fn, + "%sauthorization-post-messages.json", + GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR)); + ctx->messages = json_load_file (fn, + JSON_REJECT_DUPLICATES, + &err); + if (NULL == ctx->messages) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load messages from `%s': %s at %d:%d\n", + fn, + err.text, + err.line, + err.column); + GNUNET_free (fn); + GNUNET_free (ctx); + return NULL; + } + GNUNET_free (fn); + } + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->code_validity_period = GNUNET_TIME_UNIT_MONTHS; + plugin->code_rotation_period = GNUNET_TIME_UNIT_WEEKS; + plugin->code_retransmission_frequency + = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_DAYS, + 2); + plugin->cls = ctx; + plugin->validate = &post_validate; + plugin->start = &post_start; + plugin->process = &post_process; + plugin->cleanup = &post_cleanup; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "authorization-post", + "COMMAND", + &ctx->auth_command)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-post", + "COMMAND"); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; + } + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_post_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + struct PostContext *ctx = plugin->cls; + + GNUNET_free (ctx->auth_command); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; +} diff --git a/src/authorization/anastasis_authorization_plugin_sms.c b/src/authorization/anastasis_authorization_plugin_sms.c new file mode 100644 index 0000000..01b5f73 --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_sms.c @@ -0,0 +1,607 @@ +/* + This file is part of Anastasis + Copyright (C) 2019, 2021 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file include/anastasis_authorization_plugin_email.c + * @brief authorization plugin email based + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include +#include +#include +#include "anastasis_util_lib.h" + + +/** + * Saves the State of a authorization plugin. + */ +struct SMS_Context +{ + + /** + * Command which is executed to run the plugin (some bash script or a + * command line argument) + */ + char *auth_command; + + /** + * Regex for phone number validation. + */ + regex_t regex; + + /** + * Messages of the plugin, read from a resource file. + */ + json_t *messages; +}; + + +/** + * Saves the State of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * Public key of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Code which is sent to the user (here sent via SMS) + */ + uint64_t code; + + /** + * Our plugin context. + */ + struct SMS_Context *ctx; + + /** + * Function to call when we made progress. + */ + GNUNET_SCHEDULER_TaskCallback trigger; + + /** + * Closure for @e trigger. + */ + void *trigger_cls; + + /** + * holds the truth information + */ + char *phone_number; + + /** + * Handle to the helper process. + */ + struct GNUNET_OS_Process *child; + + /** + * Handle to wait for @e child + */ + struct GNUNET_ChildWaitHandle *cwh; + + /** + * Our client connection, set if suspended. + */ + struct MHD_Connection *connection; + + /** + * Message to send. + */ + char *msg; + + /** + * Offset of transmission in msg. + */ + size_t msg_off; + + /** + * Exit code from helper. + */ + long unsigned int exit_code; + + /** + * How did the helper die? + */ + enum GNUNET_OS_ProcessStatusType pst; + +}; + + +/** + * Obtain internationalized message @a msg_id from @a ctx using + * language preferences of @a conn. + * + * @param messages JSON object to lookup message from + * @param conn connection to lookup message for + * @param msg_id unique message ID + * @return NULL if message was not found + */ +static const char * +get_message (const json_t *messages, + struct MHD_Connection *conn, + const char *msg_id) +{ + const char *accept_lang; + + accept_lang = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == accept_lang) + accept_lang = "en_US"; + { + const char *ret; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_i18n_string (msg_id, + accept_lang, + &ret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (messages, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + return ret; + } +} + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed phone number for sending an SMS, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the phone number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure with a `struct SMS_Context` + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +sms_validate (void *cls, + struct MHD_Connection *connection, + const char *truth_mime, + const char *data, + size_t data_length) +{ + struct SMS_Context *ctx = cls; + int regex_result; + char *phone_number; + + phone_number = GNUNET_strndup (data, + data_length); + regex_result = regexec (&ctx->regex, + phone_number, + 0, + NULL, + 0); + GNUNET_free (phone_number); + if (0 != regex_result) + { + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_SMS_PHONE_INVALID, + NULL)) + return GNUNET_SYSERR; + return GNUNET_NO; + } + return GNUNET_OK; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * Sends SMS. + * + * @param cls closure with a `struct SMS_Context` + * @param trigger function to call when we made progress + * @param trigger_cls closure for @a trigger + * @param truth_uuid Identifier of the challenge, to be (if possible) included in the + * interaction with the user + * @param code secret code that the user has to provide back to satisfy the challenge in + * the main anastasis protocol + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @return state to track progress on the authorization operation, NULL on failure + */ +static struct ANASTASIS_AUTHORIZATION_State * +sms_start (void *cls, + GNUNET_SCHEDULER_TaskCallback trigger, + void *trigger_cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code, + const void *data, + size_t data_length) +{ + struct SMS_Context *ctx = cls; + struct ANASTASIS_AUTHORIZATION_State *as; + + as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); + as->trigger = trigger; + as->trigger_cls = trigger_cls; + as->ctx = ctx; + as->truth_uuid = *truth_uuid; + as->code = code; + as->phone_number = GNUNET_strndup (data, + data_length); + return as; +} + + +/** + * Function called when our SMS helper has terminated. + * + * @param cls our `struct ANASTASIS_AUHTORIZATION_State` + * @param type type of the process + * @param exit_code status code of the process + */ +static void +sms_done_cb (void *cls, + enum GNUNET_OS_ProcessStatusType type, + long unsigned int exit_code) +{ + struct ANASTASIS_AUTHORIZATION_State *as = cls; + + as->child = NULL; + as->cwh = NULL; + as->pst = type; + as->exit_code = exit_code; + MHD_resume_connection (as->connection); + as->trigger (as->trigger_cls); +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param as authorization state + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +sms_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection) +{ + MHD_RESULT mres; + const char *mime; + const char *lang; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + if (NULL == as->msg) + { + /* First time, start child process and feed pipe */ + struct GNUNET_DISK_PipeHandle *p; + struct GNUNET_DISK_FileHandle *pipe_stdin; + + p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); + if (NULL == p) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, + "pipe"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR, + p, + NULL, + NULL, + as->ctx->auth_command, + as->ctx->auth_command, + as->phone_number, + NULL); + if (NULL == as->child) + { + GNUNET_DISK_pipe_close (p); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, + "exec"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + pipe_stdin = GNUNET_DISK_pipe_detach_end (p, + GNUNET_DISK_PIPE_END_WRITE); + GNUNET_assert (NULL != pipe_stdin); + GNUNET_DISK_pipe_close (p); + { + char *tpk; + + tpk = GNUNET_STRINGS_data_to_string_alloc ( + &as->truth_uuid, + sizeof (as->truth_uuid)); + GNUNET_asprintf (&as->msg, + "A-%llu\nAnastasis\n%s", + (unsigned long long) as->code, + tpk); + GNUNET_free (tpk); + } + + { + const char *off = as->msg; + size_t left = strlen (off); + + while (0 != left) + { + ssize_t ret; + + if (0 == left) + break; + ret = GNUNET_DISK_file_write (pipe_stdin, + off, + left); + if (ret <= 0) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, + "write"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->msg_off += ret; + off += ret; + left -= ret; + } + GNUNET_DISK_file_close (pipe_stdin); + } + as->cwh = GNUNET_wait_child (as->child, + &sms_done_cb, + as); + as->connection = connection; + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if (NULL != as->cwh) + { + /* Spurious call, why are we here? */ + GNUNET_break (0); + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || + (0 != as->exit_code) ) + { + char es[32]; + + GNUNET_snprintf (es, + sizeof (es), + "%u/%d", + (unsigned int) as->exit_code, + as->pst); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_SMS_HELPER_COMMAND_FAILED, + es); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + + /* Build HTTP response */ + { + struct MHD_Response *resp; + const char *end; + size_t slen; + + slen = strlen (as->phone_number); + if (slen > 4) + end = &as->phone_number[slen - 4]; + else + end = &as->phone_number[slen / 2]; + + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + json_t *body; + + body = json_pack ("{s:I, s:s, s:s}", + "code", + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, + "hint", + TALER_ErrorCode_get_hint ( + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED), + "detail", + end); + GNUNET_break (NULL != body); + resp = TALER_MHD_make_json (body); + } + else + { + size_t reply_len; + char *reply; + + reply_len = GNUNET_asprintf (&reply, + get_message (as->ctx->messages, + connection, + "instructions"), + end); + resp = MHD_create_response_from_buffer (reply_len, + reply, + MHD_RESPMEM_MUST_COPY); + GNUNET_free (reply); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + } + mres = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; + } +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +sms_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + if (NULL != as->cwh) + { + GNUNET_wait_child_cancel (as->cwh); + as->cwh = NULL; + } + if (NULL != as->child) + { + (void) GNUNET_OS_process_kill (as->child, + SIGKILL); + GNUNET_break (GNUNET_OK == + GNUNET_OS_process_wait (as->child)); + as->child = NULL; + } + GNUNET_free (as->msg); + GNUNET_free (as->phone_number); + GNUNET_free (as); +} + + +/** + * Initialize email based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_sms_init (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin; + struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct SMS_Context *ctx; + + ctx = GNUNET_new (struct SMS_Context); + { + char *fn; + json_error_t err; + + GNUNET_asprintf (&fn, + "%sauthorization-sms-messages.json", + GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR)); + ctx->messages = json_load_file (fn, + JSON_REJECT_DUPLICATES, + &err); + if (NULL == ctx->messages) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load messages from `%s': %s at %d:%d\n", + fn, + err.text, + err.line, + err.column); + GNUNET_free (fn); + GNUNET_free (ctx); + return NULL; + } + GNUNET_free (fn); + } + { + int regex_result; + const char *regexp = "^\\+?[0-9]+$"; + + regex_result = regcomp (&ctx->regex, + regexp, + REG_EXTENDED); + if (0 != regex_result) + { + GNUNET_break (0); + json_decref (ctx->messages); + GNUNET_free (ctx); + return NULL; + } + } + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS; + plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS; + plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; + plugin->cls = ctx; + plugin->validate = &sms_validate; + plugin->start = &sms_start; + plugin->process = &sms_process; + plugin->cleanup = &sms_cleanup; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "authorization-sms", + "COMMAND", + &ctx->auth_command)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-sms", + "COMMAND"); + regfree (&ctx->regex); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; + } + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_sms_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + struct SMS_Context *ctx = plugin->cls; + + GNUNET_free (ctx->auth_command); + regfree (&ctx->regex); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; +} diff --git a/src/authorization/authorization-email-messages.json b/src/authorization/authorization-email-messages.json new file mode 100644 index 0000000..56f648c --- /dev/null +++ b/src/authorization/authorization-email-messages.json @@ -0,0 +1,10 @@ +{ + "instructions" : "Recovery TAN was sent to email %.*s@DOMAIN", + "instructions_i18n" : { + "de_DE" : "Ein Authorisierungscode wurde an %.*s@DOMAIN geschickt" + }, + "body" : "Subject: Anastasis recovery code: A-%llu\n\nThis is for challenge %s.\n", + "body_i18n" : { + "de_DE" : "Subject: Anastasis Autorisierungscode: A-%llu\n\nDies ist der Code für den Vorgang %s.\n" + } +} diff --git a/src/authorization/authorization-post-messages.json b/src/authorization/authorization-post-messages.json new file mode 100644 index 0000000..d2ac83a --- /dev/null +++ b/src/authorization/authorization-post-messages.json @@ -0,0 +1,7 @@ +{ + "instructions" : "Recovery message send to an address with ZIP code %s", + "instructions_i18n" : { + "de_DE" : "Ein Authorisierungscode wurde an eine Addresse mit der Postleitzahl %s geschickt" + }, + "body" : "Dear Customer\n\nThe Anastasis recovery code you need to\nrecover your data is A-%llu.\nThis is for challenge %s.\n\nBest regards\n\nYour Anastasis provider" +} diff --git a/src/authorization/authorization-sms-messages.json b/src/authorization/authorization-sms-messages.json new file mode 100644 index 0000000..cb45ed8 --- /dev/null +++ b/src/authorization/authorization-sms-messages.json @@ -0,0 +1,6 @@ +{ + "instructions" : "Recovery TAN send to phone number ending with %s", + "instructions_i18n" : { + "de_DE" : "Ein Authorisierungscode wurde an die Telefonnummer mit der Endung %s geschickt" + } +} -- cgit v1.2.3