diff options
author | Christian Grothoff <christian@grothoff.org> | 2021-07-30 10:38:27 +0200 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2021-07-30 10:38:27 +0200 |
commit | 7e669bcf6b6336ec429da949bcb4aa456971dba2 (patch) | |
tree | d19912f950d1cac1c38b857b7d5bdaba2289544e /src | |
download | anastasis-7e669bcf6b6336ec429da949bcb4aa456971dba2.tar.gz anastasis-7e669bcf6b6336ec429da949bcb4aa456971dba2.tar.bz2 anastasis-7e669bcf6b6336ec429da949bcb4aa456971dba2.zip |
folding history in preparation of GNU Anastasis v0.0.0 release
Diffstat (limited to 'src')
154 files changed, 42232 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..5e006a7 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,3 @@ +# This Makefile is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include +SUBDIRS = include util stasis authorization backend restclient lib testing reducer cli 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 <http://www.gnu.org/licenses/> +*/ +/** + * @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 <ltdl.h> + + +/** + * 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_authorization_plugin_email.c + * @brief authorization plugin email based + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include <taler/taler_mhd_lib.h> +#include <taler/taler_json_lib.h> +#include <regex.h> +#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 <http://www.gnu.org/licenses/> +*/ +/** + * @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 <taler/taler_mhd_lib.h> + + +/** + * 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; i<strlen (filename); i++) + { + if ( (filename[i] == ' ') || + (filename[i] == '/') ) + { + flag = true; + break; + } + } + if (flag) + return GNUNET_NO; + GNUNET_free (filename); + 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 * +file_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 ANASTASIS_AUTHORIZATION_State *as; + + as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); + as->cls = 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_authorization_plugin_post.c + * @brief authorization plugin post based + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include <taler/taler_mhd_lib.h> +#include <taler/taler_json_lib.h> +#include <jansson.h> +#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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_authorization_plugin_email.c + * @brief authorization plugin email based + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include <taler/taler_mhd_lib.h> +#include <taler/taler_json_lib.h> +#include <regex.h> +#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" + } +} diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am new file mode 100644 index 0000000..1046810 --- /dev/null +++ b/src/backend/Makefile.am @@ -0,0 +1,45 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include + +pkgcfgdir = $(prefix)/share/anastasis/config.d/ + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +pkgcfg_DATA = \ + anastasis.conf + +bin_PROGRAMS = \ + anastasis-httpd + +anastasis_httpd_SOURCES = \ + anastasis-httpd.c anastasis-httpd.h \ + anastasis-httpd_mhd.c anastasis-httpd_mhd.h \ + anastasis-httpd_policy.c anastasis-httpd_policy.h \ + anastasis-httpd_policy_upload.c \ + anastasis-httpd_truth.c anastasis-httpd_truth.h \ + anastasis-httpd_terms.c anastasis-httpd_terms.h \ + anastasis-httpd_config.c anastasis-httpd_config.h \ + anastasis-httpd_truth_upload.c + +anastasis_httpd_LDADD = \ + $(top_builddir)/src/util/libanastasisutil.la \ + $(top_builddir)/src/stasis/libanastasisdb.la \ + $(top_builddir)/src/authorization/libanastasisauthorization.la \ + -ljansson \ + -ltalermerchant \ + -ltalermhd \ + -ltalerjson \ + -ltalerutil \ + -lgnunetcurl \ + -lgnunetrest \ + -lgnunetjson \ + -lgnunetutil \ + -lmicrohttpd \ + -luuid \ + $(XLIB) + +EXTRA_DIST = \ + $(pkgcfg_DATA) diff --git a/src/backend/anastasis-httpd.c b/src/backend/anastasis-httpd.c new file mode 100644 index 0000000..56bd7c9 --- /dev/null +++ b/src/backend/anastasis-httpd.c @@ -0,0 +1,943 @@ +/* + This file is part of TALER + (C) 2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file backup/anastasis-httpd.c + * @brief HTTP serving layer intended to provide basic backup operations + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis_util_lib.h" +#include "anastasis-httpd_mhd.h" +#include "anastasis_database_lib.h" +#include "anastasis-httpd_policy.h" +#include "anastasis-httpd_truth.h" +#include "anastasis-httpd_terms.h" +#include "anastasis-httpd_config.h" + + +/** + * Backlog for listen operation on unix-domain sockets. + */ +#define UNIX_BACKLOG 500 + +/** + * Upload limit to the service, in megabytes. + */ +unsigned long long int AH_upload_limit_mb; + +/** + * Annual fee for the backup account. + */ +struct TALER_Amount AH_annual_fee; + +/** + * Fee for a truth upload. + */ +struct TALER_Amount AH_truth_upload_fee; + +/** + * Amount of insurance. + */ +struct TALER_Amount AH_insurance; + +/** + * Cost for secure question truth download. + */ +struct TALER_Amount AH_question_cost; + +/** + * Our configuration. + */ +const struct GNUNET_CONFIGURATION_Handle *AH_cfg; + +/** + * Our Taler backend to process payments. + */ +char *AH_backend_url; + +/** + * Taler currency. + */ +char *AH_currency; + +/** + * Our fulfillment URL. + */ +char *AH_fulfillment_url; + +/** + * Our business name. + */ +char *AH_business_name; + +/** + * Our server salt. + */ +struct ANASTASIS_CRYPTO_ProviderSaltP AH_server_salt; + +/** + * Number of policy uploads permitted per annual fee payment. + */ +unsigned long long AH_post_counter = 64LLU; + +/** + * Our context for making HTTP requests. + */ +struct GNUNET_CURL_Context *AH_ctx; + +/** + * Should a "Connection: close" header be added to each HTTP response? + */ +static int AH_connection_close; + +/** + * Task running the HTTP server. + */ +static struct GNUNET_SCHEDULER_Task *mhd_task; + +/** + * Global return code + */ +static int global_result; + +/** + * The MHD Daemon + */ +static struct MHD_Daemon *mhd; + +/** + * Connection handle to the our database + */ +struct ANASTASIS_DatabasePlugin *db; + +/** + * Reschedule context for #SH_ctx. + */ +static struct GNUNET_CURL_RescheduleContext *rc; + +/** + * Set if we should immediately #MHD_run again. + */ +static int triggered; + +/** + * Username and password to use for client authentication + * (optional). + */ +static char *userpass; + +/** + * Type of the client's TLS certificate (optional). + */ +static char *certtype; + +/** + * File with the client's TLS certificate (optional). + */ +static char *certfile; + +/** + * File with the client's TLS private key (optional). + */ +static char *keyfile; + +/** + * This value goes in the Authorization:-header. + */ +static char *apikey; + +/** + * Passphrase to decrypt client's TLS private key file (optional). + */ +static char *keypass; + + +/** + * Function that queries MHD's select sets and + * starts the task waiting for them. + */ +static struct GNUNET_SCHEDULER_Task * +prepare_daemon (void); + + +/** + * Call MHD to process pending requests and then go back + * and schedule the next run. + * + * @param cls the `struct MHD_Daemon` of the HTTP server to run + */ +static void +run_daemon (void *cls) +{ + (void) cls; + mhd_task = NULL; + do { + triggered = 0; + GNUNET_assert (MHD_YES == MHD_run (mhd)); + } while (0 != triggered); + mhd_task = prepare_daemon (); +} + + +/** + * Kick MHD to run now, to be called after MHD_resume_connection(). + * Basically, we need to explicitly resume MHD's event loop whenever + * we made progress serving a request. This function re-schedules + * the task processing MHD's activities to run immediately. + * + * @param cls NULL + */ +void +AH_trigger_daemon (void *cls) +{ + (void) cls; + if (NULL != mhd_task) + { + GNUNET_SCHEDULER_cancel (mhd_task); + mhd_task = GNUNET_SCHEDULER_add_now (&run_daemon, + NULL); + } + else + { + triggered = 1; + } +} + + +/** + * Kick GNUnet Curl scheduler to begin curl interactions. + */ +void +AH_trigger_curl (void) +{ + GNUNET_CURL_gnunet_scheduler_reschedule (&rc); +} + + +/** + * A client has requested the given url using the given method + * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT, + * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc). The callback + * must call MHD callbacks to provide content to give back to the + * client and return an HTTP status code (i.e. #MHD_HTTP_OK, + * #MHD_HTTP_NOT_FOUND, etc.). + * + * @param cls argument given together with the function + * pointer when the handler was registered with MHD + * @param url the requested url + * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, + * #MHD_HTTP_METHOD_PUT, etc.) + * @param version the HTTP version string (i.e. + * #MHD_HTTP_VERSION_1_1) + * @param upload_data the data being uploaded (excluding HEADERS, + * for a POST that fits into memory and that is encoded + * with a supported encoding, the POST data will NOT be + * given in upload_data and is instead available as + * part of #MHD_get_connection_values; very large POST + * data *will* be made available incrementally in + * @a upload_data) + * @param upload_data_size set initially to the size of the + * @a upload_data provided; the method must update this + * value to the number of bytes NOT processed; + * @param con_cls pointer that the callback can set to some + * address and that will be preserved by MHD for future + * calls for this request; since the access handler may + * be called many times (i.e., for a PUT/POST operation + * with plenty of upload data) this allows the application + * to easily associate some request-specific state. + * If necessary, this state can be cleaned up in the + * global #MHD_RequestCompletedCallback (which + * can be set with the #MHD_OPTION_NOTIFY_COMPLETED). + * Initially, `*con_cls` will be NULL. + * @return #MHD_YES if the connection was handled successfully, + * #MHD_NO if the socket must be closed due to a serious + * error while handling the request + */ +static MHD_RESULT +url_handler (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **con_cls) +{ + static struct AH_RequestHandler handlers[] = { + /* Landing page, tell humans to go away. */ + { "/", MHD_HTTP_METHOD_GET, "text/plain", + "Hello, I'm Anastasis. This HTTP server is not for humans.\n", 0, + &TMH_MHD_handler_static_response, MHD_HTTP_OK }, + { "/agpl", MHD_HTTP_METHOD_GET, "text/plain", + NULL, 0, + &TMH_MHD_handler_agpl_redirect, MHD_HTTP_FOUND }, + { "/terms", MHD_HTTP_METHOD_GET, NULL, + NULL, 0, + &AH_handler_terms, MHD_HTTP_OK }, + { "/privacy", MHD_HTTP_METHOD_GET, NULL, + NULL, 0, + &AH_handler_terms, MHD_HTTP_OK }, + { "/config", MHD_HTTP_METHOD_GET, "text/json", + NULL, 0, + &AH_handler_config, MHD_HTTP_OK }, + {NULL, NULL, NULL, NULL, 0, 0 } + }; + static struct AH_RequestHandler h404 = { + "", NULL, "text/html", + "<html><title>404: not found</title></html>", 0, + &TMH_MHD_handler_static_response, MHD_HTTP_NOT_FOUND + }; + static struct AH_RequestHandler h405 = { + "", NULL, "text/html", + "<html><title>405: method not allowed</title></html>", 0, + &TMH_MHD_handler_static_response, MHD_HTTP_METHOD_NOT_ALLOWED + }; + struct TM_HandlerContext *hc = *con_cls; + const char *correlation_id = NULL; + bool path_matched; + + if (NULL == hc) + { + struct GNUNET_AsyncScopeId aid; + + GNUNET_async_scope_fresh (&aid); + /* We only read the correlation ID on the first callback for every client */ + correlation_id = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + "Anastasis-Correlation-Id"); + if ((NULL != correlation_id) && + (GNUNET_YES != GNUNET_CURL_is_valid_scope_id (correlation_id))) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid incoming correlation ID\n"); + correlation_id = NULL; + } + hc = GNUNET_new (struct TM_HandlerContext); + *con_cls = hc; + hc->async_scope_id = aid; + } + if (0 == strcasecmp (method, + MHD_HTTP_METHOD_HEAD)) + method = MHD_HTTP_METHOD_GET; /* MHD will throw away the body */ + + GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id); + if (NULL != correlation_id) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling request for (%s) URL '%s', correlation_id=%s\n", + method, + url, + correlation_id); + else + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling request (%s) for URL '%s'\n", + method, + url); + if (0 == strncmp (url, + "/policy/", + strlen ("/policy/"))) + { + const char *account = url + strlen ("/policy/"); + struct ANASTASIS_CRYPTO_AccountPublicKeyP account_pub; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + account, + strlen (account), + &account_pub, + sizeof (struct ANASTASIS_CRYPTO_AccountPublicKeyP))) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "account public key"); + } + if (0 == strcmp (method, + MHD_HTTP_METHOD_GET)) + { + return AH_policy_get (connection, + &account_pub); + } + if (0 == strcmp (method, + MHD_HTTP_METHOD_POST)) + { + return AH_handler_policy_post (connection, + hc, + &account_pub, + upload_data, + upload_data_size); + } + return TMH_MHD_handler_static_response (&h405, + connection); + } + if (0 == strncmp (url, + "/truth/", + strlen ("/truth/"))) + { + struct ANASTASIS_CRYPTO_TruthUUIDP tu; + const char *pub_key_str; + + pub_key_str = &url[strlen ("/truth/")]; + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + pub_key_str, + strlen (pub_key_str), + &tu, + sizeof(tu))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "truth UUID"); + } + if (0 == strcmp (method, + MHD_HTTP_METHOD_GET)) + { + return AH_handler_truth_get (connection, + &tu, + hc); + } + if (0 == strcmp (method, + MHD_HTTP_METHOD_POST)) + { + return AH_handler_truth_post (connection, + hc, + &tu, + upload_data, + upload_data_size); + } + return TMH_MHD_handler_static_response (&h405, + connection); + } + path_matched = false; + for (unsigned int i = 0; NULL != handlers[i].url; i++) + { + struct AH_RequestHandler *rh = &handlers[i]; + + if (0 == strcmp (url, + rh->url)) + { + path_matched = true; + if (0 == strcasecmp (method, + MHD_HTTP_METHOD_OPTIONS)) + { + return TALER_MHD_reply_cors_preflight (connection); + } + if ( (NULL == rh->method) || + (0 == strcasecmp (method, + rh->method)) ) + { + return rh->handler (rh, + connection); + } + } + } + if (path_matched) + return TMH_MHD_handler_static_response (&h405, + connection); + return TMH_MHD_handler_static_response (&h404, + connection); +} + + +/** + * Shutdown task (magically invoked when the application is being + * quit) + * + * @param cls NULL + */ +static void +do_shutdown (void *cls) +{ + (void) cls; + AH_resume_all_bc (); + AH_truth_shutdown (); + AH_truth_upload_shutdown (); + if (NULL != mhd_task) + { + GNUNET_SCHEDULER_cancel (mhd_task); + mhd_task = NULL; + } + if (NULL != AH_ctx) + { + GNUNET_CURL_fini (AH_ctx); + AH_ctx = NULL; + } + if (NULL != rc) + { + GNUNET_CURL_gnunet_rc_destroy (rc); + rc = NULL; + } + if (NULL != mhd) + { + MHD_stop_daemon (mhd); + mhd = NULL; + } + if (NULL != db) + { + ANASTASIS_DB_plugin_unload (db); + db = NULL; + } +} + + +/** + * Function called whenever MHD is done with a request. If the + * request was a POST, we may have stored a `struct Buffer *` in the + * @a con_cls that might still need to be cleaned up. Call the + * respective function to free the memory. + * + * @param cls client-defined closure + * @param connection connection handle + * @param con_cls value as set by the last call to + * the #MHD_AccessHandlerCallback + * @param toe reason for request termination + * @see #MHD_OPTION_NOTIFY_COMPLETED + * @ingroup request + */ +static void +handle_mhd_completion_callback (void *cls, + struct MHD_Connection *connection, + void **con_cls, + enum MHD_RequestTerminationCode toe) +{ + struct TM_HandlerContext *hc = *con_cls; + + (void) cls; + (void) connection; + if (NULL == hc) + return; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Finished handling request with status %d\n", + (int) toe); + if (NULL != hc->cc) + hc->cc (hc); + GNUNET_free (hc); + *con_cls = NULL; +} + + +/** + * Function that queries MHD's select sets and + * starts the task waiting for them. + * + * @param daemon_handle HTTP server to prepare to run + */ +static struct GNUNET_SCHEDULER_Task * +prepare_daemon (void) +{ + struct GNUNET_SCHEDULER_Task *ret; + fd_set rs; + fd_set ws; + fd_set es; + struct GNUNET_NETWORK_FDSet *wrs; + struct GNUNET_NETWORK_FDSet *wws; + int max; + MHD_UNSIGNED_LONG_LONG timeout; + int haveto; + struct GNUNET_TIME_Relative tv; + + FD_ZERO (&rs); + FD_ZERO (&ws); + FD_ZERO (&es); + wrs = GNUNET_NETWORK_fdset_create (); + wws = GNUNET_NETWORK_fdset_create (); + max = -1; + GNUNET_assert (MHD_YES == + MHD_get_fdset (mhd, + &rs, + &ws, + &es, + &max)); + haveto = MHD_get_timeout (mhd, &timeout); + if (haveto == MHD_YES) + tv = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, + timeout); + else + tv = GNUNET_TIME_UNIT_FOREVER_REL; + GNUNET_NETWORK_fdset_copy_native (wrs, &rs, max + 1); + GNUNET_NETWORK_fdset_copy_native (wws, &ws, max + 1); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Adding run_daemon select task\n"); + ret = GNUNET_SCHEDULER_add_select (GNUNET_SCHEDULER_PRIORITY_HIGH, + tv, + wrs, + wws, + &run_daemon, + NULL); + GNUNET_NETWORK_fdset_destroy (wrs); + GNUNET_NETWORK_fdset_destroy (wws); + return ret; +} + + +/** + * Main function that will be run by the scheduler. + * + * @param cls closure + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be + * NULL!) + * @param config configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *config) +{ + int fh; + uint16_t port; + enum TALER_MHD_GlobalOptions go; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting anastasis-httpd\n"); + go = TALER_MHD_GO_NONE; + if (AH_connection_close) + go |= TALER_MHD_GO_FORCE_CONNECTION_CLOSE; + AH_load_terms (config); + TALER_MHD_setup (go); + AH_cfg = config; + global_result = GNUNET_SYSERR; + GNUNET_SCHEDULER_add_shutdown (&do_shutdown, + NULL); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (config, + "anastasis", + "UPLOAD_LIMIT_MB", + &AH_upload_limit_mb)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "UPLOAD_LIMIT_MB"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_amount (config, + "anastasis", + "INSURANCE", + &AH_insurance)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "INSURANCE"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_amount (config, + "authorization-question", + "COST", + &AH_question_cost)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-question", + "COST"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_amount (config, + "anastasis", + "ANNUAL_FEE", + &AH_annual_fee)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "ANNUAL_FEE"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_amount (config, + "anastasis", + "TRUTH_UPLOAD_FEE", + &AH_truth_upload_fee)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "TRUTH_UPLOAD_FEE"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_currency (config, + &AH_currency)) + { + GNUNET_SCHEDULER_shutdown (); + return; + } + if (0 != strcasecmp (AH_currency, + AH_annual_fee.currency)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "ANNUAL_FEE", + "currency mismatch"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_amount_cmp_currency (&AH_insurance, + &AH_annual_fee)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "INSURANCE", + "currency mismatch"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "anastasis", + "PAYMENT_BACKEND_URL", + &AH_backend_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "PAYMENT_BACKEND_URL"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if ( (0 != strncasecmp ("https://", + AH_backend_url, + strlen ("https://"))) && + (0 != strncasecmp ("http://", + AH_backend_url, + strlen ("http://"))) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "PAYMENT_BACKEND_URL", + "Must be HTTP(S) URL"); + GNUNET_SCHEDULER_shutdown (); + return; + } + + if ( (0 == strcasecmp ("https://", + AH_backend_url)) || + (0 == strcasecmp ("http://", + AH_backend_url)) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "PAYMENT_BACKEND_URL", + "Must have domain name"); + GNUNET_SCHEDULER_shutdown (); + return; + } + + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "anastasis", + "FULFILLMENT_URL", + &AH_fulfillment_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "FULFILLMENT_URL"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (config, + "anastasis", + "ANNUAL_POLICY_UPLOAD_LIMIT", + &AH_post_counter)) + { + /* only warn, we will use the default */ + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + "anastasis", + "ANNUAL_POLICY_UPLOAD_LIMIT"); + } + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "anastasis", + "BUSINESS_NAME", + &AH_business_name)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "BUSINESS_NAME"); + GNUNET_SCHEDULER_shutdown (); + return; + } + { + char *server_salt; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "anastasis", + "SERVER_SALT", + &server_salt)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "SERVER_SALT"); + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_kdf (&AH_server_salt, + sizeof (AH_server_salt), + "anastasis-server-salt", + strlen ("anastasis-server-salt"), + server_salt, + strlen (server_salt), + NULL, + 0)); + GNUNET_free (server_salt); + } + + /* setup HTTP client event loop */ + AH_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &rc); + rc = GNUNET_CURL_gnunet_rc_create (AH_ctx); + if (NULL != userpass) + GNUNET_CURL_set_userpass (AH_ctx, + userpass); + if (NULL != keyfile) + GNUNET_CURL_set_tlscert (AH_ctx, + certtype, + certfile, + keyfile, + keypass); + if (NULL != apikey) + { + char *auth_header; + + GNUNET_asprintf (&auth_header, + "%s: %s", + MHD_HTTP_HEADER_AUTHORIZATION, + apikey); + if (GNUNET_OK != + GNUNET_CURL_append_header (AH_ctx, + auth_header)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed so set %s header, trying without\n", + MHD_HTTP_HEADER_AUTHORIZATION); + } + GNUNET_free (auth_header); + } + + if (NULL == + (db = ANASTASIS_DB_plugin_load (config))) + { + GNUNET_SCHEDULER_shutdown (); + return; + } + + fh = TALER_MHD_bind (config, + "anastasis", + &port); + if ( (0 == port) && + (-1 == fh) ) + { + GNUNET_SCHEDULER_shutdown (); + return; + } + mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK, + port, + NULL, NULL, + &url_handler, NULL, + MHD_OPTION_LISTEN_SOCKET, fh, + MHD_OPTION_NOTIFY_COMPLETED, + &handle_mhd_completion_callback, NULL, + MHD_OPTION_CONNECTION_TIMEOUT, (unsigned + int) 10 /* 10s */, + MHD_OPTION_END); + if (NULL == mhd) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to launch HTTP service (port %u in use?), exiting.\n", + port); + GNUNET_SCHEDULER_shutdown (); + return; + } + global_result = GNUNET_OK; + mhd_task = prepare_daemon (); +} + + +/** + * The main function of the serve tool + * + * @param argc number of arguments from the command line + * @param argv command line arguments + * @return 0 ok, 1 on error + */ +int +main (int argc, + char *const *argv) +{ + enum GNUNET_GenericReturnValue res; + struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_string ('A', + "auth", + "USERNAME:PASSWORD", + "use the given USERNAME and PASSWORD for client authentication", + &userpass), + GNUNET_GETOPT_option_flag ('C', + "connection-close", + "force HTTP connections to be closed after each request", + &AH_connection_close), + GNUNET_GETOPT_option_string ('k', + "key", + "KEYFILE", + "file with the private TLS key for TLS client authentication", + &keyfile), + GNUNET_GETOPT_option_string ('p', + "pass", + "KEYFILEPASSPHRASE", + "passphrase needed to decrypt the TLS client private key file", + &keypass), + GNUNET_GETOPT_option_string ('K', + "apikey", + "APIKEY", + "API key to use in the HTTP request to the merchant backend", + &apikey), + GNUNET_GETOPT_option_string ('t', + "type", + "CERTTYPE", + "type of the TLS client certificate, defaults to PEM if not specified", + &certtype), + + GNUNET_GETOPT_OPTION_END + }; + + /* FIRST get the libtalerutil initialization out + of the way. Then throw that one away, and force + the ANASTASIS defaults to be used! */ + (void) TALER_project_data_default (); + GNUNET_OS_init (ANASTASIS_project_data_default ()); + res = GNUNET_PROGRAM_run (argc, argv, + "anastasis-httpd", + "Anastasis HTTP interface", + options, &run, NULL); + if (GNUNET_SYSERR == res) + return 3; + if (GNUNET_NO == res) + return 0; + return (GNUNET_OK == global_result) ? 0 : 1; +} diff --git a/src/backend/anastasis-httpd.h b/src/backend/anastasis-httpd.h new file mode 100644 index 0000000..6fe0023 --- /dev/null +++ b/src/backend/anastasis-httpd.h @@ -0,0 +1,225 @@ +/* + This file is part of TALER + Copyright (C) 2019 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis/anastasis-httpd.h + * @brief HTTP serving layer + * @author Christian Grothoff + */ +#ifndef ANASTASIS_HTTPD_H +#define ANASTASIS_HTTPD_H + +#include "platform.h" +#include "anastasis_database_lib.h" +#include <microhttpd.h> +#include <taler/taler_mhd_lib.h> +#include <gnunet/gnunet_mhd_compat.h> + + +/** + * For how many years do we allow users to store truth at most? Also + * how long we store things if the cost is zero. + */ +#define ANASTASIS_MAX_YEARS_STORAGE 5 + + +/** + * @brief Struct describing an URL and the handler for it. + */ +struct AH_RequestHandler +{ + + /** + * URL the handler is for. + */ + const char *url; + + /** + * Method the handler is for, NULL for "all". + */ + const char *method; + + /** + * Mime type to use in reply (hint, can be NULL). + */ + const char *mime_type; + + /** + * Raw data for the @e handler + */ + const void *data; + + /** + * Number of bytes in @e data, 0 for 0-terminated. + */ + size_t data_size; + + + /** + * Function to call to handle the request. + * + * @param rh this struct + * @param connection the MHD connection to handle + * @return MHD result code + */ + MHD_RESULT (*handler)(struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + + /** + * Default response code. + */ + unsigned int response_code; +}; + + +/** + * Each MHD response handler that sets the "connection_cls" to a + * non-NULL value must use a struct that has this struct as its first + * member. This struct contains a single callback, which will be + * invoked to clean up the memory when the contection is completed. + */ +struct TM_HandlerContext; + +/** + * Signature of a function used to clean up the context + * we keep in the "connection_cls" of MHD when handling + * a request. + * + * @param hc header of the context to clean up. + */ +typedef void +(*TM_ContextCleanup)(struct TM_HandlerContext *hc); + + +/** + * Each MHD response handler that sets the "connection_cls" to a + * non-NULL value must use a struct that has this struct as its first + * member. This struct contains a single callback, which will be + * invoked to clean up the memory when the connection is completed. + */ +struct TM_HandlerContext +{ + + /** + * Function to execute the handler-specific cleanup of the + * (typically larger) context. + */ + TM_ContextCleanup cc; + + /** + * Handler-specific context. + */ + void *ctx; + + /** + * Which request handler is handling this request? + */ + const struct AH_RequestHandler *rh; + + /** + * Asynchronous request context id. + */ + struct GNUNET_AsyncScopeId async_scope_id; +}; + +/** + * Handle to the database backend. + */ +extern struct ANASTASIS_DatabasePlugin *db; + +/** + * Upload limit to the service, in megabytes. + */ +extern unsigned long long AH_upload_limit_mb; + +/** + * Annual fee for the backup account. + */ +extern struct TALER_Amount AH_annual_fee; + +/** + * Fee for a truth upload. + */ +extern struct TALER_Amount AH_truth_upload_fee; + +/** + * Amount of insurance. + */ +extern struct TALER_Amount AH_insurance; + +/** + * Cost for secure question truth download. + */ +extern struct TALER_Amount AH_question_cost; + +/** + * Our Taler backend to process payments. + */ +extern char *AH_backend_url; + +/** + * Taler currency. + */ +extern char *AH_currency; + +/** + * Our configuration. + */ +extern const struct GNUNET_CONFIGURATION_Handle *AH_cfg; + +/** + * Number of policy uploads permitted per annual fee payment. + */ +extern unsigned long long AH_post_counter; + +/** + * Our fulfillment URL + */ +extern char *AH_fulfillment_url; + +/** + * Our business name. + */ +extern char *AH_business_name; + +/** + * Our server salt. + */ +extern struct ANASTASIS_CRYPTO_ProviderSaltP AH_server_salt; + +/** + * Our context for making HTTP requests. + */ +extern struct GNUNET_CURL_Context *AH_ctx; + + +/** + * Kick MHD to run now, to be called after MHD_resume_connection(). + * Basically, we need to explicitly resume MHD's event loop whenever + * we made progress serving a request. This function re-schedules + * the task processing MHD's activities to run immediately. + * + * @param cls NULL + */ +void +AH_trigger_daemon (void *cls); + +/** + * Kick GNUnet Curl scheduler to begin curl interactions. + */ +void +AH_trigger_curl (void); + +#endif diff --git a/src/backend/anastasis-httpd_config.c b/src/backend/anastasis-httpd_config.c new file mode 100644 index 0000000..fff6bcb --- /dev/null +++ b/src/backend/anastasis-httpd_config.c @@ -0,0 +1,132 @@ +/* + This file is part of Anastasis + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file backend/anastasis-httpd_config.c + * @brief headers for /terms handler + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include <jansson.h> +#include "anastasis-httpd_config.h" +#include "anastasis-httpd.h" +#include <taler/taler_json_lib.h> +#include "anastasis_authorization_lib.h" + + +/** + * Add enabled methods and their fees to the ``/config`` response. + * + * @param[in,out] cls a `json_t` array to build + * @param section configuration section to inspect + */ +static void +add_methods (void *cls, + const char *section) +{ + json_t *method_arr = cls; + struct ANASTASIS_AuthorizationPlugin *p; + struct TALER_Amount cost; + json_t *method; + + if (0 != strncasecmp (section, + "authorization-", + strlen ("authorization-"))) + return; + if (GNUNET_YES != + GNUNET_CONFIGURATION_get_value_yesno (AH_cfg, + section, + "ENABLED")) + return; + section += strlen ("authorization-"); + p = ANASTASIS_authorization_plugin_load (section, + AH_cfg, + &cost); + if (NULL == p) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load authorization plugin `%s'\n", + section); + return; + } + method = json_pack ("{s:s, s:o}", + "type", + section, + "cost", + TALER_JSON_from_amount (&cost)); + GNUNET_assert (NULL != method); + GNUNET_assert ( + 0 == + json_array_append_new (method_arr, + method)); +} + + +MHD_RESULT +AH_handler_config (struct AH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + json_t *method_arr = json_array (); + + GNUNET_assert (NULL != method_arr); + { + json_t *method; + + method = json_pack ("{s:s, s:o}", + "type", + "question", + "cost", + TALER_JSON_from_amount (&AH_question_cost)); + GNUNET_assert ( + 0 == + json_array_append_new (method_arr, + method)); + } + GNUNET_CONFIGURATION_iterate_sections (AH_cfg, + &add_methods, + method_arr); + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:s, s:s, s:s, s:s, s:o, s:I," + " s:o, s:o, s:o, s:o }", + "name", + "anastasis", + "version", + "0:0:0", + "business_name", + AH_business_name, + "currency", + (char *) AH_currency, + "methods", + method_arr, + "storage_limit_in_megabytes", + (json_int_t) AH_upload_limit_mb, + /* 6 */ + "annual_fee", + TALER_JSON_from_amount (&AH_annual_fee), + "truth_upload_fee", + TALER_JSON_from_amount ( + &AH_truth_upload_fee), + "liability_limit", + TALER_JSON_from_amount (&AH_insurance), + "server_salt", + GNUNET_JSON_from_data_auto ( + &AH_server_salt)); +} + + +/* end of anastasis-httpd_config.c */ diff --git a/src/backend/anastasis-httpd_config.h b/src/backend/anastasis-httpd_config.h new file mode 100644 index 0000000..7d58792 --- /dev/null +++ b/src/backend/anastasis-httpd_config.h @@ -0,0 +1,41 @@ +/* + This file is part of Anastasis + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file backend/anastasis-httpd_config.h + * @brief headers for /terms handler + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#ifndef ANASTASIS_HTTPD_CONFIG_H +#define ANASTASIS_HTTPD_CONFIG_H +#include <microhttpd.h> +#include "anastasis-httpd.h" + +/** + * Manages a /config call. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +AH_handler_config (struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + +#endif + +/* end of anastasis-httpd_config.h */ diff --git a/src/backend/anastasis-httpd_mhd.c b/src/backend/anastasis-httpd_mhd.c new file mode 100644 index 0000000..c39a54c --- /dev/null +++ b/src/backend/anastasis-httpd_mhd.c @@ -0,0 +1,70 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015, 2016 GNUnet e.V. and INRIA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_mhd.c + * @brief helpers for MHD interaction; these are TALER_EXCHANGE_handler_ functions + * that generate simple MHD replies that do not require any real operations + * to be performed (error handling, static pages, etc.) + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include <jansson.h> +#include "anastasis-httpd_mhd.h" + + +/** + * Function to call to handle the request by sending + * back static data from the @a rh. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +TMH_MHD_handler_static_response (struct AH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + if (0 == rh->data_size) + rh->data_size = strlen ((const char *) rh->data); + return TALER_MHD_reply_static (connection, + rh->response_code, + rh->mime_type, + (void *) rh->data, + rh->data_size); +} + + +/** + * Function to call to handle the request by sending + * back a redirect to the AGPL source code. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +TMH_MHD_handler_agpl_redirect (struct AH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + (void) rh; + return TALER_MHD_reply_agpl (connection, + "http://www.git.taler.net/anastasis.git"); +} + + +/* end of anastasis-httpd_mhd.c */ diff --git a/src/backend/anastasis-httpd_mhd.h b/src/backend/anastasis-httpd_mhd.h new file mode 100644 index 0000000..628abfa --- /dev/null +++ b/src/backend/anastasis-httpd_mhd.h @@ -0,0 +1,61 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 GNUnet e.V. and INRIA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +/** + * @file anastasis-httpd_mhd.h + * @brief helpers for MHD interaction, used to generate simple responses + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef ANASTASIS_HTTPD_MHD_H +#define ANASTASIS_HTTPD_MHD_H +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "anastasis-httpd.h" + + +/** + * Function to call to handle the request by sending + * back static data from the @a rh. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] connection_cls the connection's closure (can be updated) + * @param upload_data upload data + * @param[in,out] upload_data_size number of bytes (left) in @a upload_data + * @param mi merchant backend instance, NULL is allowed in this case! + * @return MHD result code + */ +MHD_RESULT +TMH_MHD_handler_static_response (struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + + +/** + * Function to call to handle the request by sending + * back a redirect to the AGPL source code. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +TMH_MHD_handler_agpl_redirect (struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + + +#endif diff --git a/src/backend/anastasis-httpd_policy.c b/src/backend/anastasis-httpd_policy.c new file mode 100644 index 0000000..2417e15 --- /dev/null +++ b/src/backend/anastasis-httpd_policy.c @@ -0,0 +1,252 @@ +/* + This file is part of TALER + Copyright (C) 2019, 2021 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_policy.c + * @brief functions to handle incoming requests on /policy/ + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis-httpd_policy.h" +#include "anastasis_service.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_rest_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_merchant_service.h> +#include <taler/taler_signatures.h> + +/** + * How long do we hold an HTTP client connection if + * we are awaiting payment before giving up? + */ +#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 30) + + +/** + * Return the current recoverydocument of @a account on @a connection + * using @a default_http_status on success. + * + * @param connection MHD connection to use + * @param account account to query + * @return MHD result code + */ +static MHD_RESULT +return_policy (struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub) +{ + enum GNUNET_DB_QueryStatus qs; + struct MHD_Response *resp; + struct ANASTASIS_AccountSignatureP account_sig; + struct GNUNET_HashCode recovery_data_hash; + const char *version_s; + char version_b[14]; + uint32_t version; + void *res_recovery_data; + size_t res_recovery_data_size; + + version_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "version"); + if (NULL != version_s) + { + char dummy; + + if (1 != sscanf (version_s, + "%u%c", + &version, + &dummy)) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "version"); + } + qs = db->get_recovery_document (db->cls, + account_pub, + version, + &account_sig, + &recovery_data_hash, + &res_recovery_data_size, + &res_recovery_data); + } + else + { + qs = db->get_latest_recovery_document (db->cls, + account_pub, + &account_sig, + &recovery_data_hash, + &res_recovery_data_size, + &res_recovery_data, + &version); + GNUNET_snprintf (version_b, + sizeof (version_b), + "%u", + (unsigned int) version); + version_s = version_b; + } + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_recovery_document"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_recovery_document"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_POLICY_NOT_FOUND, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* interesting case below */ + break; + } + resp = MHD_create_response_from_buffer (res_recovery_data_size, + res_recovery_data, + MHD_RESPMEM_MUST_FREE); + TALER_MHD_add_global_headers (resp); + { + char *sig_s; + char *etag; + + sig_s = GNUNET_STRINGS_data_to_string_alloc (&account_sig, + sizeof (account_sig)); + etag = GNUNET_STRINGS_data_to_string_alloc (&recovery_data_hash, + sizeof (recovery_data_hash)); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE, + sig_s)); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_POLICY_VERSION, + version_s)); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etag)); + GNUNET_free (etag); + GNUNET_free (sig_s); + } + { + MHD_RESULT ret; + + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; + } +} + + +MHD_RESULT +AH_policy_get (struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub) +{ + struct GNUNET_HashCode recovery_data_hash; + enum ANASTASIS_DB_AccountStatus as; + MHD_RESULT ret; + uint32_t version; + struct GNUNET_TIME_Absolute expiration; + + as = db->lookup_account (db->cls, + account_pub, + &expiration, + &recovery_data_hash, + &version); + switch (as) + { + case ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_SYNC_ACCOUNT_UNKNOWN, + NULL); + case ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup account"); + case ANASTASIS_DB_ACCOUNT_STATUS_NO_RESULTS: + { + struct MHD_Response *resp; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_NO_CONTENT, + resp); + MHD_destroy_response (resp); + } + return ret; + case ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED: + { + const char *inm; + + inm = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if (NULL != inm) + { + struct GNUNET_HashCode inm_h; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (inm, + strlen (inm), + &inm_h, + sizeof (inm_h))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_POLICY_BAD_IF_NONE_MATCH, + "Etag must be a base32-encoded SHA-512 hash"); + } + if (0 == GNUNET_memcmp (&inm_h, + &recovery_data_hash)) + { + struct MHD_Response *resp; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_NOT_MODIFIED, + resp); + MHD_destroy_response (resp); + return ret; + } + } + } + /* We have a result, should fetch and return it! */ + break; + } + return return_policy (connection, + account_pub); +} diff --git a/src/backend/anastasis-httpd_policy.h b/src/backend/anastasis-httpd_policy.h new file mode 100644 index 0000000..9fb630d --- /dev/null +++ b/src/backend/anastasis-httpd_policy.h @@ -0,0 +1,66 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015, 2016 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_policy.h + * @brief functions to handle incoming requests on /policy/ + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#ifndef ANASTASIS_HTTPD_POLICY_H +#define ANASTASIS_HTTPD_POLICY_H +#include <microhttpd.h> + + +/** + * Service is shutting down, resume all MHD connections NOW. + */ +void +AH_resume_all_bc (void); + + +/** + * Handle GET /policy/$ACCOUNT_PUB request. + * + * @param connection the MHD connection to handle + * @param account_pub public key of the account + * @return MHD result code + */ +MHD_RESULT +AH_policy_get (struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub); + + +/** + * Handle POST /policy/$ACCOUNT_PUB request. + * + * @param connection the MHD connection to handle + * @param con_cls the connection's closure + * @param account_pub public key of the account + * @param upload_data upload data + * @param upload_data_size number of bytes (left) in @a upload_data + * @return MHD result code + */ +MHD_RESULT +AH_handler_policy_post ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub, + const char *upload_data, + size_t *upload_data_size); + + +#endif diff --git a/src/backend/anastasis-httpd_policy_upload.c b/src/backend/anastasis-httpd_policy_upload.c new file mode 100644 index 0000000..b8bd5ed --- /dev/null +++ b/src/backend/anastasis-httpd_policy_upload.c @@ -0,0 +1,1211 @@ +/* + This file is part of TALER + Copyright (C) 2021 Anastasis SARL + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_policy.c + * @brief functions to handle incoming requests on /policy/ + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis-httpd_policy.h" +#include "anastasis_service.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_rest_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_merchant_service.h> +#include <taler/taler_signatures.h> + +/** + * How long do we hold an HTTP client connection if + * we are awaiting payment before giving up? + */ +#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 30) + + +/** + * Context for an upload operation. + */ +struct PolicyUploadContext +{ + + /** + * Signature of the account holder. + */ + struct ANASTASIS_AccountSignatureP account_sig; + + /** + * Public key of the account holder. + */ + struct ANASTASIS_CRYPTO_AccountPublicKeyP account; + + /** + * Hash of the upload we are receiving right now (as promised + * by the client, to be verified!). + */ + struct GNUNET_HashCode new_policy_upload_hash; + + /** + * Hash context for the upload. + */ + struct GNUNET_HashContext *hash_ctx; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct PolicyUploadContext *next; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct PolicyUploadContext *prev; + + /** + * Used while suspended for resumption. + */ + struct MHD_Connection *con; + + /** + * Upload, with as many bytes as we have received so far. + */ + char *upload; + + /** + * Used while we are awaiting proposal creation. + */ + struct TALER_MERCHANT_PostOrdersHandle *po; + + /** + * Used while we are waiting payment. + */ + struct TALER_MERCHANT_OrderMerchantGetHandle *cpo; + + /** + * HTTP response code to use on resume, if non-NULL. + */ + struct MHD_Response *resp; + + /** + * Order under which the client promised payment, or NULL. + */ + const char *order_id; + + /** + * Payment Identifier + */ + struct ANASTASIS_PaymentSecretP payment_identifier; + + /** + * Timestamp of the order in @e payment_identifier. Used to + * select the most recent unpaid offer. + */ + struct GNUNET_TIME_Absolute existing_pi_timestamp; + + /** + * When does the operation timeout? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * How long must the account be valid? Determines whether we should + * trigger payment, and if so how much. + */ + struct GNUNET_TIME_Absolute end_date; + + /** + * How long is the account already valid? + * Determines how much the user needs to pay. + */ + struct GNUNET_TIME_Absolute paid_until; + + /** + * Expected total upload size. + */ + size_t upload_size; + + /** + * Current offset for the upload. + */ + size_t upload_off; + + /** + * HTTP response code to use on resume, if resp is set. + */ + unsigned int response_code; + + /** + * For how many years does the client still have + * to pay? + */ + unsigned int years_to_pay; + + /** + * true if client provided a payment secret / order ID? + */ + bool payment_identifier_provided; + +}; + + +/** + * Kept in DLL for shutdown handling while suspended. + */ +static struct PolicyUploadContext *puc_head; + +/** + * Kept in DLL for shutdown handling while suspended. + */ +static struct PolicyUploadContext *puc_tail; + + +/** + * Service is shutting down, resume all MHD connections NOW. + */ +void +AH_resume_all_bc () +{ + struct PolicyUploadContext *puc; + + while (NULL != (puc = puc_head)) + { + GNUNET_CONTAINER_DLL_remove (puc_head, + puc_tail, + puc); + if (NULL != puc->po) + { + TALER_MERCHANT_orders_post_cancel (puc->po); + puc->po = NULL; + } + if (NULL != puc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (puc->cpo); + puc->cpo = NULL; + } + MHD_resume_connection (puc->con); + } +} + + +/** + * Function called to clean up a backup context. + * + * @param hc a `struct PolicyUploadContext` + */ +static void +cleanup_ctx (struct TM_HandlerContext *hc) +{ + struct PolicyUploadContext *puc = hc->ctx; + + if (NULL != puc->po) + TALER_MERCHANT_orders_post_cancel (puc->po); + if (NULL != puc->cpo) + TALER_MERCHANT_merchant_order_get_cancel (puc->cpo); + if (NULL != puc->hash_ctx) + GNUNET_CRYPTO_hash_context_abort (puc->hash_ctx); + if (NULL != puc->resp) + MHD_destroy_response (puc->resp); + GNUNET_free (puc->upload); + GNUNET_free (puc); +} + + +/** + * Transmit a payment request for @a order_id on @a connection + * + * @param connection MHD connection + * @param order_id our backend's order ID + * @return #GNUNET_OK on success + */ +static int +make_payment_request (struct PolicyUploadContext *puc) +{ + struct MHD_Response *resp; + + /* request payment via Taler */ + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + if (NULL == resp) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + TALER_MHD_add_global_headers (resp); + { + char *hdr; + char *pfx; + char *hn; + + if (0 == strncasecmp ("https://", + AH_backend_url, + strlen ("https://"))) + { + pfx = "taler://"; + hn = &AH_backend_url[strlen ("https://")]; + } + else if (0 == strncasecmp ("http://", + AH_backend_url, + strlen ("http://"))) + { + pfx = "taler+http://"; + hn = &AH_backend_url[strlen ("http://")]; + } + else + { + GNUNET_break (0); + MHD_destroy_response (resp); + return GNUNET_SYSERR; + } + if (0 == strlen (hn)) + { + GNUNET_break (0); + MHD_destroy_response (resp); + return GNUNET_SYSERR; + } + { + char *order_id; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &puc->payment_identifier, + sizeof (puc->payment_identifier)); + GNUNET_asprintf (&hdr, + "%spay/%s%s/", + pfx, + hn, + order_id); + GNUNET_free (order_id); + } + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_TALER, + hdr)); + GNUNET_free (hdr); + } + puc->resp = resp; + puc->response_code = MHD_HTTP_PAYMENT_REQUIRED; + return GNUNET_OK; +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * POST /private/orders request to a merchant. + * + * @param cls our `struct PolicyUploadContext` + * @param por response details + */ +static void +proposal_cb (void *cls, + const struct TALER_MERCHANT_PostOrdersReply *por) +{ + struct PolicyUploadContext *puc = cls; + enum GNUNET_DB_QueryStatus qs; + + puc->po = NULL; + GNUNET_CONTAINER_DLL_remove (puc_head, + puc_tail, + puc); + MHD_resume_connection (puc->con); + AH_trigger_daemon (NULL); + if (MHD_HTTP_OK != por->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Backend returned status %u/%d\n", + por->hr.http_status, + (int) por->hr.ec); + GNUNET_break (0); + puc->resp = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:I, s:I, s:O?}", + "code", + (json_int_t) TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR, + "hint", + "Failed to setup order with merchant backend", + "backend-ec", + (json_int_t) por->hr.ec, + "backend-http-status", + (json_int_t) por->hr.http_status, + "backend-reply", + por->hr.reply); + GNUNET_assert (NULL != puc->resp); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Storing payment request for order `%s'\n", + por->details.ok.order_id); + + qs = db->record_recdoc_payment (db->cls, + &puc->account, + (uint32_t) AH_post_counter, + &puc->payment_identifier, + &AH_annual_fee); + if (0 >= qs) + { + GNUNET_break (0); + puc->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "record recdoc payment"); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + if (GNUNET_OK != + make_payment_request (puc)) + { + GNUNET_break (0); + puc->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "failed to initiate payment"); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + } +} + + +/** + * Callback to process a GET /check-payment request + * + * @param cls our `struct PolicyUploadContext` + * @param hr HTTP response details + * @param osr order status + */ +static void +check_payment_cb (void *cls, + const struct TALER_MERCHANT_HttpResponse *hr, + const struct TALER_MERCHANT_OrderStatusResponse *osr) +{ + struct PolicyUploadContext *puc = cls; + + /* refunds are not supported, verify */ + puc->cpo = NULL; + GNUNET_CONTAINER_DLL_remove (puc_head, + puc_tail, + puc); + MHD_resume_connection (puc->con); + AH_trigger_daemon (NULL); + switch (hr->http_status) + { + case MHD_HTTP_OK: + GNUNET_assert (NULL != osr); + break; /* processed below */ + case MHD_HTTP_UNAUTHORIZED: + puc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED, + NULL); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + default: + puc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR, + "failed to initiate payment"); + puc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment status checked: %s\n", + osr->status ? "paid" : "unpaid"); + switch (osr->status) + { + case TALER_MERCHANT_OSC_PAID: + { + enum GNUNET_DB_QueryStatus qs; + unsigned int years; + struct GNUNET_TIME_Relative paid_until; + const json_t *contract; + struct TALER_Amount amount; + struct GNUNET_JSON_Specification cspec[] = { + TALER_JSON_spec_amount ("amount", + AH_currency, + &amount), + GNUNET_JSON_spec_end () + }; + + contract = osr->details.paid.contract_terms; + if (GNUNET_OK != + GNUNET_JSON_parse (contract, + cspec, + NULL, NULL)) + { + GNUNET_break (0); + puc->resp = TALER_MHD_make_error ( + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + "no amount given"); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; /* continue as planned */ + } + years = TALER_amount_divide2 (&amount, + &AH_annual_fee); + paid_until = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + years); + /* add 1 week grace period, otherwise if a user + wants to pay for 1 year, the first seconds + would have passed between making the payment + and our subsequent check if +1 year was + paid... So we actually say 1 year = 52 weeks + on the server, while the client calculates + with 365 days. */ + paid_until = GNUNET_TIME_relative_add (paid_until, + GNUNET_TIME_UNIT_WEEKS); + + qs = db->increment_lifetime (db->cls, + &puc->account, + &puc->payment_identifier, + paid_until, + &puc->paid_until); + if (0 <= qs) + return; /* continue as planned */ + GNUNET_break (0); + puc->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_FETCH_FAILED, + "increment lifetime"); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; /* continue as planned */ + } + case TALER_MERCHANT_OSC_UNPAID: + case TALER_MERCHANT_OSC_CLAIMED: + break; + } + if (0 != puc->existing_pi_timestamp.abs_value_us) + { + /* repeat payment request */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Repeating payment request\n"); + if (GNUNET_OK != + make_payment_request (puc)) + { + GNUNET_break (0); + puc->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "failed to initiate payment"); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + } + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Timeout waiting for payment\n"); + puc->resp = TALER_MHD_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT, + "Timeout awaiting promised payment"); + GNUNET_assert (NULL != puc->resp); + puc->response_code = MHD_HTTP_REQUEST_TIMEOUT; +} + + +/** + * Helper function used to ask our backend to await + * a payment for the user's account. + * + * @param puc context to begin payment for. + * @param timeout when to give up trying + */ +static void +await_payment (struct PolicyUploadContext *puc) +{ + struct GNUNET_TIME_Relative timeout + = GNUNET_TIME_absolute_get_remaining (puc->timeout); + + GNUNET_CONTAINER_DLL_insert (puc_head, + puc_tail, + puc); + MHD_suspend_connection (puc->con); + { + char *order_id; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &puc->payment_identifier, + sizeof(struct ANASTASIS_PaymentSecretP)); + puc->cpo = TALER_MERCHANT_merchant_order_get (AH_ctx, + AH_backend_url, + order_id, + NULL /* our payments are NOT session-bound */, + false, + timeout, + &check_payment_cb, + puc); + GNUNET_free (order_id); + } + AH_trigger_curl (); +} + + +/** + * Helper function used to ask our backend to begin processing a + * payment for the user's account. May perform asynchronous + * operations by suspending the connection if required. + * + * @param puc context to begin payment for. + * @return MHD status code + */ +static MHD_RESULT +begin_payment (struct PolicyUploadContext *puc) +{ + json_t *order; + + GNUNET_CONTAINER_DLL_insert (puc_head, + puc_tail, + puc); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending connection while creating order at `%s'\n", + AH_backend_url); + { + char *order_id; + struct TALER_Amount upload_fee; + + if (0 > + TALER_amount_multiply (&upload_fee, + &AH_annual_fee, + puc->years_to_pay)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "storage_duration_years"); + } + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &puc->payment_identifier, + sizeof(struct ANASTASIS_PaymentSecretP)); + order = json_pack ("{s:o, s:s, s:[{s:s,s:I,s:s}], s:s }", + "amount", TALER_JSON_from_amount (&upload_fee), + "summary", "Anastasis policy storage fee", + "products", + "description", "policy storage fee", + "quantity", (json_int_t) puc->years_to_pay, + "unit", "years", + "order_id", order_id); + GNUNET_free (order_id); + } + MHD_suspend_connection (puc->con); + puc->po = TALER_MERCHANT_orders_post2 (AH_ctx, + AH_backend_url, + order, + GNUNET_TIME_UNIT_ZERO, + NULL, /* no payment target */ + 0, + NULL, /* no inventory products */ + 0, + NULL, /* no uuids */ + false, /* do NOT require claim token */ + &proposal_cb, + puc); + AH_trigger_curl (); + json_decref (order); + return MHD_YES; +} + + +/** + * Prepare to receive a payment, possibly requesting it, or just waiting + * for it to be completed by the client. + * + * @param puc context to prepare payment for + * @return MHD status + */ +static MHD_RESULT +prepare_payment (struct PolicyUploadContext *puc) +{ + if (! puc->payment_identifier_provided) + { + GNUNET_CRYPTO_random_block ( + GNUNET_CRYPTO_QUALITY_NONCE, + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + puc->payment_identifier_provided = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No payment identifier, initiating payment\n"); + return begin_payment (puc); + } + await_payment (puc); + return MHD_YES; +} + + +MHD_RESULT +AH_handler_policy_post ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub, + const char *recovery_data, + size_t *recovery_data_size) +{ + struct PolicyUploadContext *puc = hc->ctx; + + if (NULL == puc) + { + /* first call, setup internals */ + puc = GNUNET_new (struct PolicyUploadContext); + hc->ctx = puc; + hc->cc = &cleanup_ctx; + puc->con = connection; + + { + const char *pay_id; + + pay_id = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER); + if (NULL != pay_id) + { + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + pay_id, + strlen (pay_id), + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER + " header must be a base32-encoded Payment-Secret"); + } + puc->payment_identifier_provided = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Policy upload started with payment identifier `%s'\n", + pay_id); + } + } + puc->account = *account_pub; + /* now setup 'puc' */ + { + const char *lens; + unsigned long len; + + lens = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_CONTENT_LENGTH); + if ( (NULL == lens) || + (1 != sscanf (lens, + "%lu", + &len)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + (NULL == lens) + ? TALER_EC_ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH + : TALER_EC_ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH, + NULL); + } + if (len / 1024 / 1024 >= AH_upload_limit_mb) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_PAYLOAD_TOO_LARGE, + TALER_EC_SYNC_MALFORMED_CONTENT_LENGTH, + "Content-length value not acceptable"); + } + puc->upload = GNUNET_malloc_large (len); + if (NULL == puc->upload) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "malloc"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_PAYLOAD_TOO_LARGE, + TALER_EC_ANASTASIS_POLICY_OUT_OF_MEMORY_ON_CONTENT_LENGTH, + NULL); + } + puc->upload_size = (size_t) len; + } + { + /* Check if header contains Anastasis-Policy-Signature */ + const char *sig_s; + + sig_s = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE); + if ( (NULL == sig_s) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (sig_s, + strlen (sig_s), + &puc->account_sig, + sizeof (puc->account_sig))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_POLICY_BAD_SIGNATURE, + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE + " header must include a base32-encoded EdDSA signature"); + } + } + { + /* Check if header contains an ETAG */ + const char *etag; + + etag = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if ( (NULL == etag) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (etag, + strlen (etag), + &puc->new_policy_upload_hash, + sizeof (puc->new_policy_upload_hash))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_POLICY_BAD_IF_MATCH, + MHD_HTTP_HEADER_IF_NONE_MATCH + " header must include a base32-encoded SHA-512 hash"); + } + } + /* validate signature */ + { + struct ANASTASIS_UploadSignaturePS usp = { + .purpose.size = htonl (sizeof (usp)), + .purpose.purpose = htonl (TALER_SIGNATURE_ANASTASIS_POLICY_UPLOAD), + .new_recovery_data_hash = puc->new_policy_upload_hash + }; + + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_ANASTASIS_POLICY_UPLOAD, + &usp, + &puc->account_sig.eddsa_sig, + &account_pub->pub)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_POLICY_BAD_SIGNATURE, + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE); + } + } + + { + const char *long_poll_timeout_ms; + + long_poll_timeout_ms = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "timeout_ms"); + if (NULL != long_poll_timeout_ms) + { + unsigned int timeout; + + if (1 != sscanf (long_poll_timeout_ms, + "%u", + &timeout)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "timeout_ms (must be non-negative number)"); + } + puc->timeout + = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_MILLISECONDS, + timeout)); + } + else + { + puc->timeout = GNUNET_TIME_relative_to_absolute + (CHECK_PAYMENT_GENERIC_TIMEOUT); + } + } + + /* check if the client insists on paying */ + { + const char *req; + unsigned int years; + + req = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "storage_duration"); + if (NULL != req) + { + char dummy; + + if (1 != sscanf (req, + "%u%c", + &years, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "storage_duration (must be non-negative number)"); + } + } + else + { + years = 0; + } + puc->end_date = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + years)); + } + + /* get ready to hash (done here as we may go async for payments next) */ + puc->hash_ctx = GNUNET_CRYPTO_hash_context_start (); + + /* Check database to see if the transaction is permissible */ + { + struct GNUNET_TIME_Relative rem; + + rem = GNUNET_TIME_absolute_get_remaining (puc->end_date); + puc->years_to_pay = rem.rel_value_us + / GNUNET_TIME_UNIT_YEARS.rel_value_us; + if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us)) + puc->years_to_pay++; + + if (puc->payment_identifier_provided) + { + /* check if payment identifier is valid (existing and paid) */ + bool paid; + bool valid_counter; + enum GNUNET_DB_QueryStatus qs; + + qs = db->check_payment_identifier (db->cls, + &puc->payment_identifier, + &paid, + &valid_counter); + if (qs < 0) + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + + if ( (! paid) || + (! valid_counter) ) + { + if (! valid_counter) + { + puc->payment_identifier_provided = false; + if (0 == puc->years_to_pay) + puc->years_to_pay = 1; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Too many uploads with this payment identifier, initiating fresh payment\n"); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Given payment identifier not known to be paid, initiating payment\n"); + } + return prepare_payment (puc); + } + } + + if (! puc->payment_identifier_provided) + { + struct TALER_Amount zero_amount; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Relative rel; + + TALER_amount_set_zero (AH_currency, + &zero_amount); + /* generate fresh payment identifier */ + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + if (0 != TALER_amount_cmp (&AH_annual_fee, + &zero_amount)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No payment identifier, requesting payment\n"); + return begin_payment (puc); + } + /* Cost is zero, fake "zero" payment having happened */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Policy upload is free, allowing upload without payment\n"); + qs = db->record_recdoc_payment (db->cls, + account_pub, + AH_post_counter, + &puc->payment_identifier, + &AH_annual_fee); + if (qs <= 0) + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + rel = GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_YEARS, + ANASTASIS_MAX_YEARS_STORAGE); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Policy lifetime is %s (%u years)\n", + GNUNET_STRINGS_relative_time_to_string (rel, + GNUNET_YES), + ANASTASIS_MAX_YEARS_STORAGE); + puc->paid_until = GNUNET_TIME_relative_to_absolute (rel); + qs = db->update_lifetime (db->cls, + account_pub, + &puc->payment_identifier, + puc->paid_until); + if (qs <= 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + } + } + + /* Check if existing policy matches upload (and if, skip it) */ + { + struct GNUNET_HashCode hc; + enum ANASTASIS_DB_AccountStatus as; + uint32_t version; + struct GNUNET_TIME_Absolute now; + struct GNUNET_TIME_Relative rem; + + as = db->lookup_account (db->cls, + account_pub, + &puc->paid_until, + &hc, + &version); + now = GNUNET_TIME_absolute_get (); + if (puc->paid_until.abs_value_us < now.abs_value_us) + puc->paid_until = now; + rem = GNUNET_TIME_absolute_get_difference (puc->paid_until, + puc->end_date); + puc->years_to_pay = rem.rel_value_us + / GNUNET_TIME_UNIT_YEARS.rel_value_us; + if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us)) + puc->years_to_pay++; + + if ( (ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED == as) && + (0 != puc->years_to_pay) ) + { + /* user requested extension, force payment */ + as = ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED; + } + switch (as) + { + case ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Expiration too low, initiating payment\n"); + return prepare_payment (puc); + case ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case ANASTASIS_DB_ACCOUNT_STATUS_NO_RESULTS: + /* continue below */ + break; + case ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED: + if (0 == GNUNET_memcmp (&hc, + &puc->new_policy_upload_hash)) + { + /* Refuse upload: we already have that backup! */ + struct MHD_Response *resp; + MHD_RESULT ret; + char version_s[14]; + + GNUNET_snprintf (version_s, + sizeof (version_s), + "%u", + (unsigned int) version); + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_POLICY_VERSION, + version_s)); + ret = MHD_queue_response (connection, + MHD_HTTP_NOT_MODIFIED, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + break; + } + } + /* ready to begin! */ + return MHD_YES; + } + + if (NULL != puc->resp) + { + MHD_RESULT ret; + + /* We generated a response asynchronously, queue that */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning asynchronously generated response with HTTP status %u\n", + puc->response_code); + ret = MHD_queue_response (connection, + puc->response_code, + puc->resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (puc->resp); + puc->resp = NULL; + return ret; + } + + /* handle upload */ + if (0 != *recovery_data_size) + { + /* check MHD invariant */ + GNUNET_assert (puc->upload_off + *recovery_data_size <= puc->upload_size); + memcpy (&puc->upload[puc->upload_off], + recovery_data, + *recovery_data_size); + puc->upload_off += *recovery_data_size; + GNUNET_CRYPTO_hash_context_read (puc->hash_ctx, + recovery_data, + *recovery_data_size); + *recovery_data_size = 0; + return MHD_YES; + } + + if ( (0 == puc->upload_off) && + (0 != puc->upload_size) && + (NULL == puc->resp) ) + { + /* wait for upload */ + return MHD_YES; + } + + /* finished with upload, check hash */ + if (NULL != puc->hash_ctx) + { + struct GNUNET_HashCode our_hash; + + GNUNET_CRYPTO_hash_context_finish (puc->hash_ctx, + &our_hash); + puc->hash_ctx = NULL; + if (0 != GNUNET_memcmp (&our_hash, + &puc->new_policy_upload_hash)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_POLICY_INVALID_UPLOAD, + "Data uploaded does not match Etag promise"); + } + } + + /* store backup to database */ + { + enum ANASTASIS_DB_StoreStatus ss; + uint32_t version = UINT32_MAX; + char version_s[14]; + char expir_s[32]; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Uploading recovery document\n"); + ss = db->store_recovery_document (db->cls, + &puc->account, + &puc->account_sig, + &puc->new_policy_upload_hash, + puc->upload, + puc->upload_size, + &puc->payment_identifier, + &version); + GNUNET_snprintf (version_s, + sizeof (version_s), + "%u", + (unsigned int) version); + GNUNET_snprintf (expir_s, + sizeof (expir_s), + "%llu", + (unsigned long long) + (puc->paid_until.abs_value_us + / GNUNET_TIME_UNIT_SECONDS.rel_value_us)); + switch (ss) + { + case ANASTASIS_DB_STORE_STATUS_STORE_LIMIT_EXCEEDED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Storage request limit exceeded, requesting payment\n"); + if (! puc->payment_identifier_provided) + { + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + puc->payment_identifier_provided = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Also no payment identifier, requesting payment\n"); + } + return begin_payment (puc); + case ANASTASIS_DB_STORE_STATUS_PAYMENT_REQUIRED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Policy store operation requires payment\n"); + if (! puc->payment_identifier_provided) + { + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + puc->payment_identifier_provided = true; + } + return begin_payment (puc); + case ANASTASIS_DB_STORE_STATUS_HARD_ERROR: + case ANASTASIS_DB_STORE_STATUS_SOFT_ERROR: + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case ANASTASIS_DB_STORE_STATUS_NO_RESULTS: + { + /* database says nothing actually changed, 304 (could + theoretically happen if another equivalent upload succeeded + since we last checked!) */ + struct MHD_Response *resp; + MHD_RESULT ret; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + "Anastasis-Version", + version_s)); + ret = MHD_queue_response (connection, + MHD_HTTP_NOT_MODIFIED, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + case ANASTASIS_DB_STORE_STATUS_SUCCESS: + /* generate main (204) standard success reply */ + { + struct MHD_Response *resp; + MHD_RESULT ret; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_POLICY_VERSION, + version_s)); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_POLICY_EXPIRATION, + expir_s)); + ret = MHD_queue_response (connection, + MHD_HTTP_NO_CONTENT, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + } + } + GNUNET_break (0); + return MHD_NO; +} diff --git a/src/backend/anastasis-httpd_terms.c b/src/backend/anastasis-httpd_terms.c new file mode 100644 index 0000000..6be5690 --- /dev/null +++ b/src/backend/anastasis-httpd_terms.c @@ -0,0 +1,98 @@ +/* + This file is part of Anastasis + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file backend/anastasis-httpd_terms.c + * @brief headers for /terms handler + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis-httpd_terms.h" +#include <taler/taler_json_lib.h> + +/** + * Our terms of service. + */ +static struct TALER_MHD_Legal *tos; + + +/** + * Our privacy policy. + */ +static struct TALER_MHD_Legal *pp; + + +/** + * Manages a /terms call. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +AH_handler_terms (struct AH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + (void) rh; + return TALER_MHD_reply_legal (connection, + tos); +} + + +/** + * Handle a "/privacy" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +AH_handler_privacy (const struct AH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + (void) rh; + return TALER_MHD_reply_legal (connection, + pp); +} + + +/** + * Load our terms of service as per configuration. + * + * @param cfg configuration to process + */ +void +AH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + tos = TALER_MHD_legal_load (cfg, + "anastasis", + "TERMS_DIR", + "TERMS_ETAG"); + if (NULL == tos) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Terms of service not configured\n"); + pp = TALER_MHD_legal_load (cfg, + "anastasis", + "PRIVACY_DIR", + "PRIVACY_ETAG"); + if (NULL == pp) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Privacy policy not configured\n"); +} + + +/* end of anastasis-httpd_terms.c */ diff --git a/src/backend/anastasis-httpd_terms.h b/src/backend/anastasis-httpd_terms.h new file mode 100644 index 0000000..dc59d41 --- /dev/null +++ b/src/backend/anastasis-httpd_terms.h @@ -0,0 +1,62 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file backend/anastasis-httpd_terms.h + * @brief headers for /terms handler + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#ifndef ANASTASIS_HTTPD_TERMS_H +#define ANASTASIS_HTTPD_TERMS_H +#include <microhttpd.h> +#include "anastasis-httpd.h" + +/** + * Manages a /terms call. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +AH_handler_terms (struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + + +/** + * Handle a "/privacy" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +AH_handler_privacy (const struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + +/** + * Load our terms of service as per configuration. + * + * @param cfg configuration to process + */ +void +AH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg); + + +#endif + +/* end of anastasis-httpd_terms.h */ diff --git a/src/backend/anastasis-httpd_truth.c b/src/backend/anastasis-httpd_truth.c new file mode 100644 index 0000000..164c33a --- /dev/null +++ b/src/backend/anastasis-httpd_truth.c @@ -0,0 +1,1428 @@ +/* + This file is part of TALER + Copyright (C) 2019, 2021 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_truth.c + * @brief functions to handle incoming requests on /truth + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis_service.h" +#include "anastasis-httpd_truth.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_rest_lib.h> +#include "anastasis_authorization_lib.h" +#include <taler/taler_merchant_service.h> +#include <taler/taler_json_lib.h> + +/** + * What is the maximum frequency at which we allow + * clients to attempt to answer security questions? + */ +#define MAX_QUESTION_FREQ GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_SECONDS, 30) + +/** + * How long do we hold an HTTP client connection if + * we are awaiting payment before giving up? + */ +#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 30) + +/** + * How long should the wallet check for auto-refunds before giving up? + */ +#define AUTO_REFUND_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 2) + + +/** + * How many retries do we allow per code? + */ +#define INITIAL_RETRY_COUNTER 3 + +struct GetContext +{ + + /** + * Payment Identifier + */ + struct ANASTASIS_PaymentSecretP payment_identifier; + + /** + * Public key of the challenge which is solved. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Key to decrypt the truth. + */ + struct ANASTASIS_CRYPTO_TruthKeyP truth_key; + + /** + * true if client provided a payment secret / order ID? + */ + struct TALER_Amount challenge_cost; + + /** + * Our handler context. + */ + struct TM_HandlerContext *hc; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct GetContext *next; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct GetContext *prev; + + /** + * Connection handle for closing or resuming + */ + struct MHD_Connection *connection; + + /** + * Reference to the authorization plugin which was loaded + */ + struct ANASTASIS_AuthorizationPlugin *authorization; + + /** + * Status of the authorization + */ + struct ANASTASIS_AUTHORIZATION_State *as; + + /** + * Used while we are awaiting proposal creation. + */ + struct TALER_MERCHANT_PostOrdersHandle *po; + + /** + * Used while we are waiting payment. + */ + struct TALER_MERCHANT_OrderMerchantGetHandle *cpo; + + /** + * HTTP response code to use on resume, if non-NULL. + */ + struct MHD_Response *resp; + + /** + * How long do we wait at most for payment? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * Random authorization code we are using. + */ + uint64_t code; + + /** + * HTTP response code to use on resume, if resp is set. + */ + unsigned int response_code; + + /** + * true if client provided a payment secret / order ID? + */ + bool payment_identifier_provided; + + /** + * True if this entry is in the #gc_head DLL. + */ + bool in_list; + + /** + * True if this entry is currently suspended. + */ + bool suspended; + + /** + * Did the request include a response? + */ + bool have_response; + +}; + +/** + * Information we track for refunds. + */ +struct RefundEntry +{ + /** + * Kept in a DLL. + */ + struct RefundEntry *next; + + /** + * Kept in a DLL. + */ + struct RefundEntry *prev; + + /** + * Operation handle. + */ + struct TALER_MERCHANT_OrderRefundHandle *ro; + + /** + * Which order is being refunded. + */ + char *order_id; + + /** + * Payment Identifier + */ + struct ANASTASIS_PaymentSecretP payment_identifier; + + /** + * Public key of the challenge which is solved. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; +}; + + +/** + * Head of linked list of active refund operations. + */ +static struct RefundEntry *re_head; + +/** + * Tail of linked list of active refund operations. + */ +static struct RefundEntry *re_tail; + +/** + * Head of linked list over all authorization processes + */ +static struct GetContext *gc_head; + +/** + * Tail of linked list over all authorization processes + */ +static struct GetContext *gc_tail; + + +void +AH_truth_shutdown (void) +{ + struct GetContext *gc; + struct RefundEntry *re; + + while (NULL != (re = re_head)) + { + GNUNET_CONTAINER_DLL_remove (re_head, + re_tail, + re); + if (NULL != re->ro) + { + TALER_MERCHANT_post_order_refund_cancel (re->ro); + re->ro = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refund `%s' failed due to shutdown\n", + re->order_id); + GNUNET_free (re->order_id); + GNUNET_free (re); + } + + while (NULL != (gc = gc_head)) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + if (NULL != gc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (gc->cpo); + gc->cpo = NULL; + } + if (NULL != gc->po) + { + TALER_MERCHANT_orders_post_cancel (gc->po); + gc->po = NULL; + } + if (gc->suspended) + { + MHD_resume_connection (gc->connection); + gc->suspended = false; + } + if (NULL != gc->as) + { + gc->authorization->cleanup (gc->as); + gc->as = NULL; + gc->authorization = NULL; + } + } + ANASTASIS_authorization_plugin_shutdown (); +} + + +/** + * Callback to process a POST /orders/ID/refund request + * + * @param cls closure + * @param http_status HTTP status code for this request + * @param ec taler-specific error code + * @param taler_refund_uri the refund uri offered to the wallet + * @param h_contract hash of the contract a Browser may need to authorize + * obtaining the HTTP response. + */ +static void +refund_cb ( + void *cls, + const struct TALER_MERCHANT_HttpResponse *hr, + const char *taler_refund_uri, + const struct GNUNET_HashCode *h_contract) +{ + struct RefundEntry *re = cls; + + re->ro = NULL; + switch (hr->http_status) + { + case MHD_HTTP_OK: + { + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Refund `%s' succeeded\n", + re->order_id); + qs = db->record_challenge_refund (db->cls, + &re->truth_uuid, + &re->payment_identifier); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refund `%s' failed with HTTP status %u: %s (#%u)\n", + re->order_id, + hr->http_status, + hr->hint, + (unsigned int) hr->ec); + break; + } + GNUNET_CONTAINER_DLL_remove (re_head, + re_tail, + re); + GNUNET_free (re->order_id); + GNUNET_free (re); +} + + +/** + * Start to give a refund for the challenge created by @a gc. + * + * @param gc request where we failed and should now grant a refund for + */ +static void +begin_refund (const struct GetContext *gc) +{ + struct RefundEntry *re; + + re = GNUNET_new (struct RefundEntry); + re->order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Challenge execution failed, triggering refund for order `%s'\n", + re->order_id); + re->payment_identifier = gc->payment_identifier; + re->truth_uuid = gc->truth_uuid; + re->ro = TALER_MERCHANT_post_order_refund (AH_ctx, + AH_backend_url, + re->order_id, + &gc->challenge_cost, + "failed to issue challenge", + &refund_cb, + re); + if (NULL == re->ro) + { + GNUNET_break (0); + GNUNET_free (re->order_id); + GNUNET_free (re); + return; + } + GNUNET_CONTAINER_DLL_insert (re_head, + re_tail, + re); +} + + +/** + * Callback used to notify the application about completed requests. + * Cleans up the requests data structures. + * + * @param hc + */ +static void +request_done (struct TM_HandlerContext *hc) +{ + struct GetContext *gc = hc->ctx; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request completed\n"); + if (NULL == gc) + return; + hc->cc = NULL; + GNUNET_assert (! gc->suspended); + if (gc->in_list) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + } + if (NULL != gc->as) + { + gc->authorization->cleanup (gc->as); + gc->authorization = NULL; + gc->as = NULL; + } + if (NULL != gc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (gc->cpo); + gc->cpo = NULL; + } + if (NULL != gc->po) + { + TALER_MERCHANT_orders_post_cancel (gc->po); + gc->po = NULL; + } + GNUNET_free (gc); + hc->ctx = NULL; +} + + +/** + * Transmit a payment request for @a order_id on @a connection + * + * @param gc context to make payment request for + */ +static void +make_payment_request (struct GetContext *gc) +{ + struct MHD_Response *resp; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + GNUNET_assert (NULL != resp); + TALER_MHD_add_global_headers (resp); + { + char *hdr; + char *order_id; + const char *pfx; + const char *hn; + + if (0 == strncasecmp ("https://", + AH_backend_url, + strlen ("https://"))) + { + pfx = "taler://"; + hn = &AH_backend_url[strlen ("https://")]; + } + else if (0 == strncasecmp ("http://", + AH_backend_url, + strlen ("http://"))) + { + pfx = "taler+http://"; + hn = &AH_backend_url[strlen ("http://")]; + } + else + { + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0); + } + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0 != strlen (hn)); + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_asprintf (&hdr, + "%spay/%s%s/", + pfx, + hn, + order_id); + GNUNET_free (order_id); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Sending payment request `%s'\n", + hdr); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_TALER, + hdr)); + GNUNET_free (hdr); + } + gc->resp = resp; + gc->response_code = MHD_HTTP_PAYMENT_REQUIRED; +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * /contract request to a merchant. + * + * @param cls our `struct GetContext` + * @param por response details + */ +static void +proposal_cb (void *cls, + const struct TALER_MERCHANT_PostOrdersReply *por) +{ + struct GetContext *gc = cls; + enum GNUNET_DB_QueryStatus qs; + + gc->po = NULL; + GNUNET_assert (gc->in_list); + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + GNUNET_assert (gc->suspended); + MHD_resume_connection (gc->connection); + gc->suspended = false; + AH_trigger_daemon (NULL); + if (MHD_HTTP_OK != por->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Backend returned status %u/%d\n", + por->hr.http_status, + (int) por->hr.ec); + GNUNET_break (0); + gc->resp = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:I, s:I, s:O?}", + "code", + (json_int_t) TALER_EC_ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR, + "hint", + "Failed to setup order with merchant backend", + "backend-ec", + (json_int_t) por->hr.ec, + "backend-http-status", + (json_int_t) por->hr.http_status, + "backend-reply", + por->hr.reply); + GNUNET_assert (NULL != gc->resp); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + } + qs = db->record_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier, + &gc->challenge_cost); + if (0 >= qs) + { + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "record challenge payment"); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Setup fresh order, creating payment request\n"); + make_payment_request (gc); +} + + +/** + * Callback to process a GET /check-payment request + * + * @param cls our `struct GetContext` + * @param hr HTTP response details + * @param osr order status + */ +static void +check_payment_cb (void *cls, + const struct TALER_MERCHANT_HttpResponse *hr, + const struct TALER_MERCHANT_OrderStatusResponse *osr) + +{ + struct GetContext *gc = cls; + + gc->cpo = NULL; + GNUNET_assert (gc->in_list); + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + GNUNET_assert (gc->suspended); + MHD_resume_connection (gc->connection); + gc->suspended = false; + AH_trigger_daemon (NULL); + + switch (hr->http_status) + { + case MHD_HTTP_OK: + GNUNET_assert (NULL != osr); + break; + case MHD_HTTP_NOT_FOUND: + /* We created this order before, how can it be not found now? */ + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_ANASTASIS_TRUTH_ORDER_DISAPPEARED, + NULL); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + case MHD_HTTP_BAD_GATEWAY: + gc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD, + NULL); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + case MHD_HTTP_GATEWAY_TIMEOUT: + gc->resp = TALER_MHD_make_error (TALER_EC_ANASTASIS_GENERIC_BACKEND_TIMEOUT, + "Timeout check payment status"); + GNUNET_assert (NULL != gc->resp); + gc->response_code = MHD_HTTP_GATEWAY_TIMEOUT; + return; + default: + { + char status[14]; + + GNUNET_snprintf (status, + sizeof (status), + "%u", + hr->http_status); + gc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS, + status); + GNUNET_assert (NULL != gc->resp); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + } + + switch (osr->status) + { + case TALER_MERCHANT_OSC_PAID: + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->update_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier); + if (0 <= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order has been paid, continuing with request processing\n"); + return; /* continue as planned */ + } + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "update challenge payment"); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; /* continue as planned */ + } + case TALER_MERCHANT_OSC_CLAIMED: + case TALER_MERCHANT_OSC_UNPAID: + /* repeat payment request */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order remains unpaid, sending payment request again\n"); + make_payment_request (gc); + return; + } + /* should never get here */ + GNUNET_break (0); +} + + +/** + * Helper function used to ask our backend to begin processing a + * payment for the user's account. May perform asynchronous + * operations by suspending the connection if required. + * + * @param gc context to begin payment for. + * @return MHD status code + */ +static MHD_RESULT +begin_payment (struct GetContext *gc) +{ + enum GNUNET_DB_QueryStatus qs; + char *order_id; + + qs = db->lookup_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup challenge payment"); + } + GNUNET_assert (! gc->in_list); + gc->in_list = true; + GNUNET_CONTAINER_DLL_insert (gc_tail, + gc_head, + gc); + GNUNET_assert (! gc->suspended); + gc->suspended = true; + MHD_suspend_connection (gc->connection); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + /* We already created the order, check if it was paid */ + struct GNUNET_TIME_Relative timeout; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order exists, checking payment status for order `%s'\n", + order_id); + timeout = GNUNET_TIME_absolute_get_remaining (gc->timeout); + gc->cpo = TALER_MERCHANT_merchant_order_get (AH_ctx, + AH_backend_url, + order_id, + NULL /* NOT session-bound */, + false, + timeout, + &check_payment_cb, + gc); + } + else + { + /* Create a fresh order */ + json_t *order; + struct GNUNET_TIME_Absolute pay_deadline; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, + &gc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Creating fresh order `%s'\n", + order_id); + pay_deadline = GNUNET_TIME_relative_to_absolute ( + ANASTASIS_CHALLENGE_OFFER_LIFETIME); + GNUNET_TIME_round_abs (&pay_deadline); + order = json_pack ("{s:o, s:s, s:s, s:o, s:o}", + "amount", TALER_JSON_from_amount (&gc->challenge_cost), + "summary", "challenge fee for anastasis service", + "order_id", order_id, + "auto_refund", GNUNET_JSON_from_time_rel ( + AUTO_REFUND_TIMEOUT), + "pay_deadline", GNUNET_JSON_from_time_abs ( + pay_deadline)); + gc->po = TALER_MERCHANT_orders_post2 (AH_ctx, + AH_backend_url, + order, + AUTO_REFUND_TIMEOUT, + NULL, /* no payment target */ + 0, + NULL, /* no inventory products */ + 0, + NULL, /* no uuids */ + false, /* do NOT require claim token */ + &proposal_cb, + gc); + json_decref (order); + } + GNUNET_free (order_id); + AH_trigger_curl (); + return MHD_YES; +} + + +/** + * Load encrypted keyshare from db and return it to the client. + * + * @param truth_uuid UUID to the truth for the looup + * @param connection the connection to respond upon + * @return MHD status code + */ +static MHD_RESULT +return_key_share ( + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct MHD_Connection *connection) +{ + struct ANASTASIS_CRYPTO_EncryptedKeyShareP encrypted_keyshare; + + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->get_key_share (db->cls, + truth_uuid, + &encrypted_keyshare); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get key share"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_TRUTH_KEY_SHARE_GONE, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning key share\n"); + { + struct MHD_Response *resp; + MHD_RESULT ret; + + resp = MHD_create_response_from_buffer (sizeof (encrypted_keyshare), + &encrypted_keyshare, + MHD_RESPMEM_MUST_COPY); + TALER_MHD_add_global_headers (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; + } +} + + +/** + * Run the authorization method-specific 'process' function and continue + * based on its result with generating an HTTP response. + * + * @param connection the connection we are handling + * @param gc our overall handler context + */ +static MHD_RESULT +run_authorization_process (struct MHD_Connection *connection, + struct GetContext *gc) +{ + enum ANASTASIS_AUTHORIZATION_Result ret; + enum GNUNET_DB_QueryStatus qs; + + ret = gc->authorization->process (gc->as, + connection); + switch (ret) + { + case ANASTASIS_AUTHORIZATION_RES_SUCCESS: + /* Challenge sent successfully */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Authorization request sent successfully\n"); + qs = db->mark_challenge_sent (db->cls, + &gc->payment_identifier, + &gc->truth_uuid, + gc->code); + GNUNET_break (0 < qs); + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_YES; + case ANASTASIS_AUTHORIZATION_RES_FAILED: + if (gc->payment_identifier_provided) + { + begin_refund (gc); + } + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_YES; + case ANASTASIS_AUTHORIZATION_RES_SUSPENDED: + /* connection was suspended again, odd that this happens */ + return MHD_YES; + case ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED: + /* Challenge sent successfully */ + qs = db->mark_challenge_sent (db->cls, + &gc->payment_identifier, + &gc->truth_uuid, + gc->code); + GNUNET_break (0 < qs); + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_NO; + case ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED: + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_NO; + } + GNUNET_break (0); + return MHD_NO; +} + + +/** + * @param connection the MHD connection to handle + * @param url handles a URL of the format "/truth/$UUID[&response=$RESPONSE]" + * @param hc + * @return MHD result code + */ +MHD_RESULT +AH_handler_truth_get ( + struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct TM_HandlerContext *hc) +{ + struct GetContext *gc = hc->ctx; + struct GNUNET_HashCode challenge_response; + void *encrypted_truth; + size_t encrypted_truth_size; + void *decrypted_truth; + size_t decrypted_truth_size; + char *truth_mime = NULL; + bool is_question; + + if (NULL == gc) + { + /* Fresh request, do initial setup */ + gc = GNUNET_new (struct GetContext); + gc->hc = hc; + hc->ctx = gc; + gc->connection = connection; + gc->truth_uuid = *truth_uuid; + gc->hc->cc = &request_done; + { + const char *pay_id; + + pay_id = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER); + if (NULL != pay_id) + { + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + pay_id, + strlen (pay_id), + &gc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER); + } + gc->payment_identifier_provided = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client provided payment identifier `%s'\n", + pay_id); + } + } + + { + /* check if header contains Truth-Decryption-Key */ + const char *tdk; + + tdk = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY); + if (NULL == tdk) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY); + } + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + tdk, + strlen (tdk), + &gc->truth_key, + sizeof (struct ANASTASIS_CRYPTO_TruthKeyP))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY); + } + } + + { + const char *challenge_response_s; + + challenge_response_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "response"); + if ( (NULL != challenge_response_s) && + (GNUNET_OK != + GNUNET_CRYPTO_hash_from_string (challenge_response_s, + &challenge_response)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "response"); + } + gc->have_response = (NULL != challenge_response_s); + } + + { + const char *long_poll_timeout_ms; + + long_poll_timeout_ms = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "timeout_ms"); + if (NULL != long_poll_timeout_ms) + { + unsigned int timeout; + + if (1 != sscanf (long_poll_timeout_ms, + "%u", + &timeout)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "timeout_ms (must be non-negative number)"); + } + gc->timeout + = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_MILLISECONDS, + timeout)); + } + else + { + gc->timeout = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_UNIT_SECONDS); + } + } + + } /* end of first-time initialization (if NULL == gc) */ + else + { + if (NULL != gc->resp) + { + MHD_RESULT ret; + + /* We generated a response asynchronously, queue that */ + ret = MHD_queue_response (connection, + gc->response_code, + gc->resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (gc->resp); + gc->resp = NULL; + return ret; + } + if (NULL != gc->as) + { + /* Authorization process is "running", check what is going on */ + GNUNET_assert (NULL != gc->authorization); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Continuing with running the authorization process\n"); + return run_authorization_process (connection, + gc); + + } + /* We get here if the async check for payment said this request + was indeed paid! */ + } + + { + /* load encrypted truth from DB */ + enum GNUNET_DB_QueryStatus qs; + char *method; + + qs = db->get_escrow_challenge (db->cls, + &gc->truth_uuid, + &encrypted_truth, + &encrypted_truth_size, + &truth_mime, + &method); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get escrow challenge"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_TRUTH_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + is_question = (0 == strcmp ("question", + method)); + if (! is_question) + { + gc->authorization + = ANASTASIS_authorization_plugin_load (method, + AH_cfg, + &gc->challenge_cost); + if (NULL == gc->authorization) + { + MHD_RESULT ret; + + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED, + method); + GNUNET_free (encrypted_truth); + GNUNET_free (truth_mime); + GNUNET_free (method); + return ret; + } + } + else + { + gc->challenge_cost = AH_question_cost; + } + GNUNET_free (method); + } + + { + struct TALER_Amount zero_amount; + + TALER_amount_set_zero (AH_currency, + &zero_amount); + if (0 != TALER_amount_cmp (&gc->challenge_cost, + &zero_amount)) + { + /* Check database to see if the transaction is paid for */ + enum GNUNET_DB_QueryStatus qs; + bool paid; + + if (! gc->payment_identifier_provided) + { + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Beginning payment, client did not provide payment identifier\n"); + return begin_payment (gc); + } + qs = db->check_challenge_payment (db->cls, + &gc->payment_identifier, + &gc->truth_uuid, + &paid); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "check challenge payment"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Create fresh payment identifier (cannot trust client) */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client-provided payment identifier is unknown.\n"); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return begin_payment (gc); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (! paid) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment identifier known. Checking payment with client's payment identifier\n"); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return begin_payment (gc); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment confirmed\n"); + break; + } + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request is free of charge\n"); + } + } + + /* We've been paid, now validate response */ + { + /* decrypt encrypted_truth */ + ANASTASIS_CRYPTO_truth_decrypt (&gc->truth_key, + encrypted_truth, + encrypted_truth_size, + &decrypted_truth, + &decrypted_truth_size); + GNUNET_free (encrypted_truth); + } + if (NULL == decrypted_truth) + { + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_TRUTH_DECRYPTION_FAILED, + NULL); + } + + /* Special case for secure question: we do not generate a numeric challenge, + but check that the hash matches */ + if (is_question) + { + if (! gc->have_response) + { + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, + NULL); + } + + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute rt; + uint64_t code; + enum ANASTASIS_DB_CodeStatus cs; + struct GNUNET_HashCode hc; + + rt = GNUNET_TIME_UNIT_FOREVER_ABS; + qs = db->create_challenge_code (db->cls, + &gc->truth_uuid, + MAX_QUESTION_FREQ, + GNUNET_TIME_UNIT_HOURS, + INITIAL_RETRY_COUNTER, + &rt, + &code); + if (0 > qs) + { + GNUNET_break (0 < qs); + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "create_challenge_code (for rate limiting)"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, + NULL); + } + /* decrement trial counter */ + ANASTASIS_hash_answer (code + 1, /* always use wrong answer */ + &hc); + cs = db->verify_challenge_code (db->cls, + &gc->truth_uuid, + &hc); + switch (cs) + { + case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: + /* good, what we wanted */ + break; + case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: + case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "verify_challenge_code"); + case ANASTASIS_DB_CODE_STATUS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, + NULL); + case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: + /* this should be impossible, we used code+1 */ + GNUNET_assert (0); + } + } + if ( (decrypted_truth_size != sizeof (challenge_response)) || + (0 != memcmp (&challenge_response, + decrypted_truth, + decrypted_truth_size)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Wrong answer provided to secure question had %u bytes, wanted %u\n", + (unsigned int) decrypted_truth_size, + (unsigned int) sizeof (challenge_response)); + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, + NULL); + } + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + return return_key_share (&gc->truth_uuid, + connection); + } + + /* Not security question, check for answer in DB */ + if (gc->have_response) + { + enum ANASTASIS_DB_CodeStatus cs; + + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + cs = db->verify_challenge_code (db->cls, + &gc->truth_uuid, + &challenge_response); + switch (cs) + { + case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Provided response does not match our stored challenge\n"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, + NULL); + case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: + case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "verify_challenge_code"); + case ANASTASIS_DB_CODE_STATUS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "No challenge known (challenge is invalidated after %u requests)\n", + INITIAL_RETRY_COUNTER); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, + NULL); + case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: + return return_key_share (&gc->truth_uuid, + connection); + } + GNUNET_break (0); + return MHD_NO; + } + + /* Not security question and no answer: use plugin to check if + decrypted truth is a valid challenge! */ + { + enum GNUNET_GenericReturnValue ret; + + ret = gc->authorization->validate (gc->authorization->cls, + connection, + truth_mime, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (truth_mime); + switch (ret) + { + case GNUNET_OK: + /* data valid, continued below */ + break; + case GNUNET_NO: + /* data invalid, reply was queued */ + GNUNET_free (decrypted_truth); + return MHD_YES; + case GNUNET_SYSERR: + /* data invalid, reply was NOT queued */ + GNUNET_free (decrypted_truth); + return MHD_NO; + } + } + + /* Setup challenge and begin authorization process */ + { + struct GNUNET_TIME_Absolute transmission_date; + enum GNUNET_DB_QueryStatus qs; + + qs = db->create_challenge_code (db->cls, + &gc->truth_uuid, + gc->authorization->code_rotation_period, + gc->authorization->code_validity_period, + INITIAL_RETRY_COUNTER, + &transmission_date, + &gc->code); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_free (decrypted_truth); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "store_challenge_code"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* 0 == retry_counter of existing challenge => rate limit exceeded */ + GNUNET_free (decrypted_truth); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* challenge code was stored successfully*/ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Created fresh challenge\n"); + break; + } + + if (GNUNET_TIME_absolute_get_duration (transmission_date).rel_value_us < + gc->authorization->code_retransmission_frequency.rel_value_us) + { + /* Too early for a retransmission! */ + GNUNET_free (decrypted_truth); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_ALREADY_REPORTED, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_ACTIVE, + NULL); + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Beginning authorization process\n"); + gc->as = gc->authorization->start (gc->authorization->cls, + &AH_trigger_daemon, + NULL, + &gc->truth_uuid, + gc->code, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (decrypted_truth); + if (NULL == gc->as) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + NULL); + } + GNUNET_assert (! gc->in_list); + gc->in_list = true; + GNUNET_CONTAINER_DLL_insert (gc_head, + gc_tail, + gc); + return run_authorization_process (connection, + gc); +} diff --git a/src/backend/anastasis-httpd_truth.h b/src/backend/anastasis-httpd_truth.h new file mode 100644 index 0000000..7a1b95f --- /dev/null +++ b/src/backend/anastasis-httpd_truth.h @@ -0,0 +1,75 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015, 2016, 2021 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_truth.h + * @brief functions to handle incoming requests on /truth + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#ifndef ANASTASIS_HTTPD_TRUTH_H +#define ANASTASIS_HTTPD_TRUTH_H +#include <microhttpd.h> + + +/** + * Prepare all active GET truth requests for system shutdown. + */ +void +AH_truth_shutdown (void); + + +/** + * Prepare all active POST truth requests for system shutdown. + */ +void +AH_truth_upload_shutdown (void); + + +/** + * Handle a GET to /truth/$UUID + * + * @param connection the MHD connection to handle + * @param truth_uuid the truth UUID + * @param con_cls + * @return MHD result code + */ +MHD_RESULT +AH_handler_truth_get ( + struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct TM_HandlerContext *hc); + + +/** + * Handle a POST to /truth/$UUID. + * + * @param connection the MHD connection to handle + * @param con_cls the connection's closure + * @param truth_uuid the truth UUID + * @param truth_data truth data + * @param truth_data_size number of bytes (left) in @a truth_data + * @return MHD result code + */ +int +AH_handler_truth_post ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const char *truth_data, + size_t *truth_data_size); + +#endif diff --git a/src/backend/anastasis-httpd_truth_upload.c b/src/backend/anastasis-httpd_truth_upload.c new file mode 100644 index 0000000..9767087 --- /dev/null +++ b/src/backend/anastasis-httpd_truth_upload.c @@ -0,0 +1,855 @@ +/* + This file is part of TALER + Copyright (C) 2019, 2021 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_truth_upload.c + * @brief functions to handle incoming POST request on /truth + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis_service.h" +#include "anastasis-httpd_truth.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_rest_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_merchant_service.h> +#include <taler/taler_signatures.h> +#include "anastasis_authorization_lib.h" + + +/** + * Information we track per truth upload. + */ +struct TruthUploadContext +{ + + /** + * UUID of the truth object we are processing. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct TruthUploadContext *next; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct TruthUploadContext *prev; + + /** + * Used while we are awaiting proposal creation. + */ + struct TALER_MERCHANT_PostOrdersHandle *po; + + /** + * Used while we are waiting payment. + */ + struct TALER_MERCHANT_OrderMerchantGetHandle *cpo; + + /** + * Post parser context. + */ + void *post_ctx; + + /** + * Handle to the client request. + */ + struct MHD_Connection *connection; + + /** + * Incoming JSON, NULL if not yet available. + */ + json_t *json; + + /** + * HTTP response code to use on resume, if non-NULL. + */ + struct MHD_Response *resp; + + /** + * When should this request time out? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * Fee that is to be paid for this upload. + */ + struct TALER_Amount upload_fee; + + /** + * HTTP response code to use on resume, if resp is set. + */ + unsigned int response_code; + + /** + * For how many years must the customer still pay? + */ + unsigned int years_to_pay; + +}; + + +/** + * Head of linked list over all truth upload processes + */ +static struct TruthUploadContext *tuc_head; + +/** + * Tail of linked list over all truth upload processes + */ +static struct TruthUploadContext *tuc_tail; + + +void +AH_truth_upload_shutdown (void) +{ + struct TruthUploadContext *tuc; + + while (NULL != (tuc = tuc_head)) + { + GNUNET_CONTAINER_DLL_remove (tuc_head, + tuc_tail, + tuc); + if (NULL != tuc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (tuc->cpo); + tuc->cpo = NULL; + } + if (NULL != tuc->po) + { + TALER_MERCHANT_orders_post_cancel (tuc->po); + tuc->po = NULL; + } + MHD_resume_connection (tuc->connection); + } +} + + +/** + * Function called to clean up a `struct TruthUploadContext`. + * + * @param hc general handler context + */ +static void +cleanup_truth_post (struct TM_HandlerContext *hc) +{ + struct TruthUploadContext *tuc = hc->ctx; + + TALER_MHD_parse_post_cleanup_callback (tuc->post_ctx); + if (NULL != tuc->po) + TALER_MERCHANT_orders_post_cancel (tuc->po); + if (NULL != tuc->cpo) + TALER_MERCHANT_merchant_order_get_cancel (tuc->cpo); + if (NULL != tuc->resp) + MHD_destroy_response (tuc->resp); + if (NULL != tuc->json) + json_decref (tuc->json); + GNUNET_free (tuc); +} + + +/** + * Transmit a payment request for @a tuc. + * + * @param tuc upload context to generate payment request for + */ +static void +make_payment_request (struct TruthUploadContext *tuc) +{ + struct MHD_Response *resp; + + /* request payment via Taler */ + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + GNUNET_assert (NULL != resp); + TALER_MHD_add_global_headers (resp); + { + char *hdr; + const char *pfx; + const char *hn; + + if (0 == strncasecmp ("https://", + AH_backend_url, + strlen ("https://"))) + { + pfx = "taler://"; + hn = &AH_backend_url[strlen ("https://")]; + } + else if (0 == strncasecmp ("http://", + AH_backend_url, + strlen ("http://"))) + { + pfx = "taler+http://"; + hn = &AH_backend_url[strlen ("http://")]; + } + else + { + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0); + } + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0 != strlen (hn)); + { + char *order_id; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &tuc->truth_uuid, + sizeof (tuc->truth_uuid)); + GNUNET_asprintf (&hdr, + "%spay/%s%s/", + pfx, + hn, + order_id); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Returning %u %s\n", + MHD_HTTP_PAYMENT_REQUIRED, + order_id); + GNUNET_free (order_id); + } + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_TALER, + hdr)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "TRUTH payment request made: %s\n", + hdr); + GNUNET_free (hdr); + } + tuc->resp = resp; + tuc->response_code = MHD_HTTP_PAYMENT_REQUIRED; +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * POST /private/orders request to a merchant. + * + * @param cls our `struct TruthUploadContext` + * @param por response details + */ +static void +proposal_cb (void *cls, + const struct TALER_MERCHANT_PostOrdersReply *por) +{ + struct TruthUploadContext *tuc = cls; + + tuc->po = NULL; + GNUNET_CONTAINER_DLL_remove (tuc_head, + tuc_tail, + tuc); + MHD_resume_connection (tuc->connection); + AH_trigger_daemon (NULL); + if (MHD_HTTP_OK != por->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Backend returned status %u/%d\n", + por->hr.http_status, + (int) por->hr.ec); + GNUNET_break (0); + tuc->resp = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:I, s:I, s:O?}", + "code", + (json_int_t) TALER_EC_ANASTASIS_GENERIC_ORDER_CREATE_BACKEND_ERROR, + "hint", + "Failed to setup order with merchant backend", + "backend-ec", + (json_int_t) por->hr.ec, + "backend-http-status", + (json_int_t) por->hr.http_status, + "backend-reply", + por->hr.reply); + GNUNET_assert (NULL != tuc->resp); + tuc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + } + make_payment_request (tuc); +} + + +/** + * Callback to process a GET /check-payment request + * + * @param cls our `struct PolicyUploadContext` + * @param hr HTTP response details + * @param osr order status + */ +static void +check_payment_cb (void *cls, + const struct TALER_MERCHANT_HttpResponse *hr, + const struct TALER_MERCHANT_OrderStatusResponse *osr) +{ + struct TruthUploadContext *tuc = cls; + + tuc->cpo = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Checking backend order status returned %u\n", + hr->http_status); + switch (hr->http_status) + { + case 0: + /* Likely timeout, complain! */ + tuc->response_code = MHD_HTTP_GATEWAY_TIMEOUT; + tuc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_GENERIC_BACKEND_TIMEOUT, + NULL); + break; + case MHD_HTTP_OK: + switch (osr->status) + { + case TALER_MERCHANT_OSC_PAID: + { + enum GNUNET_DB_QueryStatus qs; + unsigned int years; + struct GNUNET_TIME_Relative paid_until; + const json_t *contract; + struct TALER_Amount amount; + struct GNUNET_JSON_Specification cspec[] = { + TALER_JSON_spec_amount ("amount", + AH_currency, + &amount), + GNUNET_JSON_spec_end () + }; + + contract = osr->details.paid.contract_terms; + if (GNUNET_OK != + GNUNET_JSON_parse (contract, + cspec, + NULL, NULL)) + { + GNUNET_break (0); + tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + tuc->resp = TALER_MHD_make_error ( + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + "contract terms in database are malformed"); + break; + } + years = TALER_amount_divide2 (&amount, + &AH_truth_upload_fee); + paid_until = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + years); + /* add 1 week grace period, otherwise if a user + wants to pay for 1 year, the first seconds + would have passed between making the payment + and our subsequent check if +1 year was + paid... So we actually say 1 year = 52 weeks + on the server, while the client calculates + with 365 days. */ + paid_until = GNUNET_TIME_relative_add (paid_until, + GNUNET_TIME_UNIT_WEEKS); + qs = db->record_truth_upload_payment ( + db->cls, + &tuc->truth_uuid, + &osr->details.paid.deposit_total, + paid_until); + if (qs <= 0) + { + GNUNET_break (0); + tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + tuc->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "record_truth_upload_payment"); + break; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Payment confirmed, resuming upload\n"); + break; + case TALER_MERCHANT_OSC_UNPAID: + case TALER_MERCHANT_OSC_CLAIMED: + make_payment_request (tuc); + break; + } + break; + case MHD_HTTP_UNAUTHORIZED: + /* Configuration issue, complain! */ + tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + tuc->resp = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:I, s:I, s:O?}", + "code", + (json_int_t) TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED, + "hint", + TALER_ErrorCode_get_hint ( + TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED), + "backend-ec", + (json_int_t) hr->ec, + "backend-http-status", + (json_int_t) hr->http_status, + "backend-reply", + hr->reply); + GNUNET_assert (NULL != tuc->resp); + break; + case MHD_HTTP_NOT_FOUND: + /* Setup fresh order */ + { + char *order_id; + json_t *order; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &tuc->truth_uuid, + sizeof(tuc->truth_uuid)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "%u, setting up fresh order %s\n", + MHD_HTTP_NOT_FOUND, + order_id); + order = json_pack ("{s:o, s:s, s:[{s:s,s:I,s:s}], s:s}", + "amount", + TALER_JSON_from_amount (&tuc->upload_fee), + "summary", + "Anastasis challenge storage fee", + "products", + "description", "challenge storage fee", + "quantity", (json_int_t) tuc->years_to_pay, + "unit", "years", + + "order_id", + order_id); + GNUNET_free (order_id); + tuc->po = TALER_MERCHANT_orders_post2 (AH_ctx, + AH_backend_url, + order, + GNUNET_TIME_UNIT_ZERO, + NULL, /* no payment target */ + 0, + NULL, /* no inventory products */ + 0, + NULL, /* no uuids */ + false, /* do NOT require claim token */ + &proposal_cb, + tuc); + AH_trigger_curl (); + json_decref (order); + return; + } + default: + /* Unexpected backend response */ + tuc->response_code = MHD_HTTP_BAD_GATEWAY; + tuc->resp = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:I, s:I, s:O?}", + "code", + (json_int_t) TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR, + "hint", + TALER_ErrorCode_get_hint (TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR), + "backend-ec", + (json_int_t) hr->ec, + "backend-http-status", + (json_int_t) hr->http_status, + "backend-reply", + hr->reply); + break; + } + GNUNET_CONTAINER_DLL_remove (tuc_head, + tuc_tail, + tuc); + MHD_resume_connection (tuc->connection); + AH_trigger_daemon (NULL); +} + + +/** + * Helper function used to ask our backend to begin processing a + * payment for the truth upload. May perform asynchronous operations + * by suspending the connection if required. + * + * @param tuc context to begin payment for. + * @return MHD status code + */ +static MHD_RESULT +begin_payment (struct TruthUploadContext *tuc) +{ + char *order_id; + struct GNUNET_TIME_Relative timeout; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Checking backend order status...\n"); + timeout = GNUNET_TIME_absolute_get_remaining (tuc->timeout); + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &tuc->truth_uuid, + sizeof (tuc->truth_uuid)); + tuc->cpo = TALER_MERCHANT_merchant_order_get (AH_ctx, + AH_backend_url, + order_id, + NULL /* our payments are NOT session-bound */, + false, + timeout, + &check_payment_cb, + tuc); + GNUNET_free (order_id); + if (NULL == tuc->cpo) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (tuc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED, + "Could not check order status"); + } + GNUNET_CONTAINER_DLL_insert (tuc_head, + tuc_tail, + tuc); + MHD_suspend_connection (tuc->connection); + return MHD_YES; +} + + +int +AH_handler_truth_post ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const char *truth_data, + size_t *truth_data_size) +{ + struct TruthUploadContext *tuc = hc->ctx; + MHD_RESULT ret; + int res; + struct ANASTASIS_CRYPTO_EncryptedKeyShareP keyshare_data; + void *encrypted_truth; + size_t encrypted_truth_size; + const char *truth_mime; + const char *type; + enum GNUNET_DB_QueryStatus qs; + uint32_t storage_years; + struct GNUNET_TIME_Absolute paid_until; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("keyshare_data", + &keyshare_data), + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_varsize ("encrypted_truth", + &encrypted_truth, + &encrypted_truth_size), + GNUNET_JSON_spec_string ("truth_mime", + &truth_mime), + GNUNET_JSON_spec_uint32 ("storage_duration_years", + &storage_years), + GNUNET_JSON_spec_end () + }; + + if (NULL == tuc) + { + tuc = GNUNET_new (struct TruthUploadContext); + tuc->connection = connection; + tuc->truth_uuid = *truth_uuid; + hc->ctx = tuc; + hc->cc = &cleanup_truth_post; + + /* check for excessive upload */ + { + const char *lens; + unsigned long len; + char dummy; + + lens = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_CONTENT_LENGTH); + if ( (NULL == lens) || + (1 != sscanf (lens, + "%lu%c", + &len, + &dummy)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + (NULL == lens) + ? TALER_EC_ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH + : TALER_EC_ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH, + NULL); + } + if (len / 1024 / 1024 >= AH_upload_limit_mb) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_PAYLOAD_TOO_LARGE, + TALER_EC_SYNC_MALFORMED_CONTENT_LENGTH, + "Content-length value not acceptable"); + } + } + + { + const char *long_poll_timeout_ms; + + long_poll_timeout_ms = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "timeout_ms"); + if (NULL != long_poll_timeout_ms) + { + unsigned int timeout; + + if (1 != sscanf (long_poll_timeout_ms, + "%u", + &timeout)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "timeout_ms (must be non-negative number)"); + } + tuc->timeout + = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_MILLISECONDS, + timeout)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Long polling for %u ms enabled\n", + timeout); + } + else + { + tuc->timeout = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_UNIT_SECONDS); + } + } + + } /* end 'if (NULL == tuc)' */ + + if (NULL != tuc->resp) + { + /* We generated a response asynchronously, queue that */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Returning asynchronously generated response with HTTP status %u\n", + tuc->response_code); + ret = MHD_queue_response (connection, + tuc->response_code, + tuc->resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (tuc->resp); + tuc->resp = NULL; + return ret; + } + + if (NULL == tuc->json) + { + res = TALER_MHD_parse_post_json (connection, + &tuc->post_ctx, + truth_data, + truth_data_size, + &tuc->json); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; + } + if ( (GNUNET_NO == res) || + (NULL == tuc->json) ) + return MHD_YES; + } + res = TALER_MHD_parse_json_data (connection, + tuc->json, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + + /* check method is supported */ + { + struct TALER_Amount dummy; + + if ( (0 != strcmp ("question", + type)) && + (NULL == + ANASTASIS_authorization_plugin_load (type, + AH_cfg, + &dummy)) ) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_TRUTH_UPLOAD_METHOD_NOT_SUPPORTED, + type); + } + } + + if (storage_years > ANASTASIS_MAX_YEARS_STORAGE) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "storage_duration_years"); + } + if (0 == storage_years) + storage_years = 1; + + { + struct TALER_Amount zero_amount; + + TALER_amount_set_zero (AH_currency, + &zero_amount); + if (0 != TALER_amount_cmp (&AH_truth_upload_fee, + &zero_amount)) + { + struct GNUNET_TIME_Absolute desired_until; + enum GNUNET_DB_QueryStatus qs; + + desired_until + = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + storage_years)); + qs = db->check_truth_upload_paid (db->cls, + truth_uuid, + &paid_until); + if (qs < 0) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + if ( (0 == qs) || + (paid_until.abs_value_us < desired_until.abs_value_us) ) + { + struct GNUNET_TIME_Absolute now; + struct GNUNET_TIME_Relative rem; + + now = GNUNET_TIME_absolute_get (); + if (paid_until.abs_value_us < now.abs_value_us) + paid_until = now; + rem = GNUNET_TIME_absolute_get_difference (paid_until, + desired_until); + tuc->years_to_pay = rem.rel_value_us + / GNUNET_TIME_UNIT_YEARS.rel_value_us; + if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us)) + tuc->years_to_pay++; + if (0 > + TALER_amount_multiply (&tuc->upload_fee, + &AH_truth_upload_fee, + tuc->years_to_pay)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "storage_duration_years"); + } + if ( (0 != tuc->upload_fee.fraction) || + (0 != tuc->upload_fee.value) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Truth upload payment required (%d)!\n", + qs); + return begin_payment (tuc); + } + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "TRUTH paid until %s (%d)!\n", + GNUNET_STRINGS_relative_time_to_string ( + GNUNET_TIME_absolute_get_remaining ( + paid_until), + GNUNET_YES), + qs); + } + else + { + paid_until + = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + ANASTASIS_MAX_YEARS_STORAGE)); + } + } + + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Storing truth until %s!\n", + GNUNET_STRINGS_absolute_time_to_string (paid_until)); + qs = db->store_truth (db->cls, + truth_uuid, + &keyshare_data, + truth_mime, + encrypted_truth, + encrypted_truth_size, + type, + GNUNET_TIME_absolute_get_remaining (paid_until)); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "store_truth"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + { + void *xtruth; + size_t xtruth_size; + char *xtruth_mime; + char *xmethod; + bool ok = false; + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + db->get_escrow_challenge (db->cls, + truth_uuid, + &xtruth, + &xtruth_size, + &xtruth_mime, + &xmethod)) + { + ok = ( (xtruth_size == encrypted_truth_size) && + (0 == strcmp (xmethod, + type)) && + (0 == strcmp (truth_mime, + xtruth_mime)) && + (0 == memcmp (xtruth, + encrypted_truth, + xtruth_size)) ); + GNUNET_free (encrypted_truth); + GNUNET_free (xtruth_mime); + GNUNET_free (xmethod); + } + if (! ok) + { + GNUNET_JSON_parse_free (spec); + + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS, + NULL); + } + /* idempotency detected, intentional fall through! */ + } + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + { + struct MHD_Response *resp; + + GNUNET_JSON_parse_free (spec); + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_NO_CONTENT, + resp); + MHD_destroy_response (resp); + GNUNET_break (MHD_YES == ret); + return ret; + } + } + GNUNET_JSON_parse_free (spec); + GNUNET_break (0); + return MHD_NO; +} diff --git a/src/backend/anastasis.conf b/src/backend/anastasis.conf new file mode 100644 index 0000000..ddc1a65 --- /dev/null +++ b/src/backend/anastasis.conf @@ -0,0 +1,77 @@ +# This file is in the public domain. + +# These are default/sample settings for a merchant backend. + + +# General settings for the backend. +[anastasis] + +# Use TCP or UNIX domain sockets? +SERVE = tcp + +# Which HTTP port does the backend listen on? Only used if "SERVE" is 'tcp'. +PORT = 9977 + +# Which IP address should we bind to? i.e. 127.0.0.1 or ::1 for loopback. +# Can also be given as a hostname. We will bind to the wildcard (dual-stack) +# if left empty. Only used if "SERVE" is 'tcp'. +# BIND_TO = + + +# Which unix domain path should we bind to? Only used if "SERVE" is 'unix'. +UNIXPATH = ${ANASTASIS_RUNTIME_DIR}/backend.http +# What should be the file access permissions (see chmod) for "UNIXPATH"? +UNIXPATH_MODE = 660 + +# Which database backend do we use? +DB = postgres + +# Annual fee for an account +# ANNUAL_FEE = TESTKUDOS:0.1 + +# Number of policy uploads included in one annual fee payment +ANNUAL_POLICY_UPLÄOAD_LIMIT = 64 + +# Insurance +# INSURANCE = TESTKUDOS:1.0 + +# Upload limit per backup, in megabytes +UPLOAD_LIMIT_MB = 16 + +# Authentication costs + +# Cost of authentication by question +#QUESTION_COST = EUR:0 + +# Cost of authentication by file (only for testing purposes) +#FILE_COST = EUR:1 + +# Cost of authentication by E-Mail +#EMAIL_COST = EUR:0 + +# Cost of authentication by SMS +#SMS_COST = EUR:0 + +# Cost of authentication by postal +#POSTAL_COST = EUR:0 + +# Cost of authentication by video +#VIDEO_COST = EUR:0 + +#SMS authentication command which is executed +#SMSAUTH_COMMAND = some_sms_script.sh + +#E-Mail authentication command which is executed +#EMAILAUTH_COMMAND = some_email_script.sh + +# Fulfillment URL of the ANASTASIS service itself. +FULFILLMENT_URL = taler://fulfillment-success + +# Base URL of our payment backend +# PAYMENT_BACKEND_URL = http://localhost:9976/ + +# Server salt 16 Byte +# SERVER_SALT = gUfO1KGOKYIFlFQg + +# Supported methods +SUPPORTED_METHODS = question diff --git a/src/cli/.gitignore b/src/cli/.gitignore new file mode 100644 index 0000000..dbf01fa --- /dev/null +++ b/src/cli/.gitignore @@ -0,0 +1,6 @@ +*.log +anastasis-reducer +test_reducer_home +*.trs +taler-bank.err +wallet.err diff --git a/src/cli/Makefile.am b/src/cli/Makefile.am new file mode 100644 index 0000000..6b8bf23 --- /dev/null +++ b/src/cli/Makefile.am @@ -0,0 +1,56 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include + +bin_PROGRAMS = \ + anastasis-reducer + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +check_SCRIPTS = \ + test_anastasis_reducer_initialize_state.sh \ + test_anastasis_reducer_select_continent.sh \ + test_anastasis_reducer_select_country.sh \ + test_anastasis_reducer_backup_enter_user_attributes.sh \ + test_anastasis_reducer_add_authentication.sh \ + test_anastasis_reducer_done_authentication.sh \ + test_anastasis_reducer_done_policy_review.sh \ + test_anastasis_reducer_enter_secret.sh \ + test_anastasis_reducer_recovery_enter_user_attributes.sh + + +AM_TESTS_ENVIRONMENT=export ANASTASIS_PREFIX=$${ANASTASIS_PREFIX:-@libdir@};export PATH=$${ANASTASIS_PREFIX:-@prefix@}/bin:$$PATH;unset XDG_DATA_HOME;unset XDG_CONFIG_HOME; + +TESTS = \ + $(check_SCRIPTS) + +EXTRA_DIST = \ + $(check_SCRIPTS) \ + test_reducer.conf \ + test_anastasis_reducer_1.conf \ + test_anastasis_reducer_2.conf \ + test_anastasis_reducer_3.conf \ + test_anastasis_reducer_4.conf \ + resources/00-backup.json \ + resources/01-backup.json \ + resources/02-backup.json \ + resources/03-backup.json \ + resources/04-backup.json \ + resources/00-recovery.json \ + resources/01-recovery.json \ + resources/02-recovery.json + +anastasis_reducer_SOURCES = \ + anastasis-cli-redux.c +anastasis_reducer_LDADD = \ + $(top_builddir)/src/util/libanastasisutil.la \ + $(top_builddir)/src/reducer/libanastasisredux.la \ + -ltalerjson \ + -ltalerutil \ + -lgnunetjson \ + -lgnunetcurl \ + -lgnunetutil \ + -ljansson \ + $(XLIB) diff --git a/src/cli/anastasis-cli-redux.c b/src/cli/anastasis-cli-redux.c new file mode 100644 index 0000000..7b533c2 --- /dev/null +++ b/src/cli/anastasis-cli-redux.c @@ -0,0 +1,366 @@ +/* + This file is part of Anastasis + Copyright (C) 2020,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 <http://www.gnu.org/licenses/> +*/ +/** + * @file cli/anastasis-cli-redux.c + * @brief command line tool for our reducer + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ + +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "anastasis_redux.h" +#include <taler/taler_util.h> +#include <taler/taler_error_codes.h> +#include <taler/taler_json_lib.h> +#include "anastasis_util_lib.h" + +/** + * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule(). + */ +static struct GNUNET_CURL_RescheduleContext *rc; + +/** + * Curl context for communication with anastasis backend + */ +static struct GNUNET_CURL_Context *ctx; + +/** + * -b option given. + */ +static int b_flag; + +/** + * -r option given. + */ +static int r_flag; + +/** + * Input to -a option given. + */ +static char *input; + +/** + * Output filename, if given. + */ +static char *output_filename; + +/** + * JSON containing previous state + */ +static json_t *prev_state; + +/** + * JSON containing arguments for action + */ +static json_t *arguments; + +/** + * Handle to an ongoing action. + */ +static struct ANASTASIS_ReduxAction *ra; + +/** + * Return value from main. + */ +static int global_ret; + + +/** + * Persist a json state, report errors. + * + * @param state to persist + * @param filename where to write the state to, NULL for stdout + */ +static void +persist_new_state (json_t *state, + const char *filename) +{ + if (NULL != filename) + { + if (0 != + json_dump_file (state, + filename, + JSON_COMPACT)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not dump state to `%s'\n", + filename); + return; + } + return; + } + { + char *state_str = json_dumps (state, + JSON_COMPACT); + if (-1 >= + fprintf (stdout, + "%s", + state_str)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not dump state to stdout\n"); + GNUNET_free (state_str); + return; + } + GNUNET_free (state_str); + } +} + + +/** + * Function called with the results of #ANASTASIS_backup_action + * or #ANASTASIS_recovery_action. + * + * @param cls closure + * @param error_code Error code + * @param new_state new state as result + */ +static void +action_cb (void *cls, + enum TALER_ErrorCode error_code, + json_t *result_state) +{ + (void) cls; + ra = NULL; + if (NULL != result_state) + persist_new_state (result_state, + output_filename); + if (TALER_EC_NONE != error_code) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Redux failed with error %d: %s\n", + error_code, + TALER_ErrorCode_get_hint (error_code)); + } + GNUNET_SCHEDULER_shutdown (); + global_ret = (TALER_EC_NONE != error_code) ? 1 : 0; +} + + +/** + * @brief Shutdown the application. + * + * @param cls closure + */ +static void +shutdown_task (void *cls) +{ + (void) cls; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Shutdown initiated\n"); + if (NULL != ra) + { + ANASTASIS_redux_action_cancel (ra); + ra = NULL; + } + ANASTASIS_redux_done (); + if (NULL != ctx) + { + GNUNET_CURL_fini (ctx); + ctx = NULL; + } + if (NULL != rc) + { + GNUNET_CURL_gnunet_rc_destroy (rc); + rc = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Shutdown complete\n"); +} + + +/** + * @brief Start the application + * + * @param cls closure + * @param args arguments left + * @param cfgfile config file name + * @param cfg handle for the configuration file + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + (void) cls; + json_error_t error; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting anastasis-reducer\n"); + GNUNET_SCHEDULER_add_shutdown (&shutdown_task, + NULL); + if (b_flag && r_flag) + { + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "We cannot start backup and recovery at the same time!\n"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (r_flag) + { + json_t *init_state; + + init_state = ANASTASIS_recovery_start (cfg); + if (NULL == init_state) + { + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "Failed to create an initial recovery state!\n"); + GNUNET_SCHEDULER_shutdown (); + return; + } + persist_new_state (init_state, + args[0]); + json_decref (init_state); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (b_flag) + { + json_t *init_state; + + init_state = ANASTASIS_backup_start (cfg); + if (NULL == init_state) + { + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "Failed to create an initial backup state!\n"); + GNUNET_SCHEDULER_shutdown (); + return; + } + persist_new_state (init_state, + args[0]); + json_decref (init_state); + GNUNET_SCHEDULER_shutdown (); + return; + } + + /* action processing */ + { + const char *action = args[0]; + + if (NULL == action) + { + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "You must specify an action as the first argument (or `-b' or `-r')\n"); + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "Example: anastasis-reducer back\n"); + GNUNET_SCHEDULER_shutdown (); + return; + } + args++; + if (NULL != input) + { + arguments = json_loads (input, + JSON_DECODE_ANY, + &error); + if (NULL == arguments) + { + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "Failed to parse arguments on line %u:%u: %s!\n", + error.line, + error.column, + error.text); + GNUNET_SCHEDULER_shutdown (); + return; + } + } + if (NULL != args[0]) + { + prev_state = json_load_file (args[0], + JSON_DECODE_ANY, + &error); + args++; + } + else + { + prev_state = json_loadf (stdin, + JSON_DECODE_ANY, + &error); + } + if (NULL == prev_state) + { + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "Failed to parse initial state on line %u:%u: %s!\n", + error.line, + error.column, + error.text); + GNUNET_SCHEDULER_shutdown (); + return; + } + output_filename = args[0]; + /* initialize HTTP client event loop */ + ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &rc); + rc = GNUNET_CURL_gnunet_rc_create (ctx); + ANASTASIS_redux_init (ctx); + ra = ANASTASIS_redux_action (prev_state, + action, + arguments, + &action_cb, + cls); + } +} + + +int +main (int argc, + char *const *argv) +{ + /* the available command line options */ + struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_flag ('b', + "backup", + "use reducer to handle states for backup process", + &b_flag), + GNUNET_GETOPT_option_flag ('r', + "restore", + "use reducer to handle states for restore process", + &r_flag), + GNUNET_GETOPT_option_string ('a', + "arguments", + "JSON", + "pass a JSON string containing arguments to reducer", + &input), + + GNUNET_GETOPT_OPTION_END + }; + enum GNUNET_GenericReturnValue ret; + + /* FIRST get the libtalerutil initialization out + of the way. Then throw that one away, and force + the SYNC defaults to be used! */ + (void) TALER_project_data_default (); + GNUNET_OS_init (ANASTASIS_project_data_default ()); + ret = GNUNET_PROGRAM_run (argc, + argv, + "anastasis-reducer", + "This is an application for using Anastasis to handle the states.\n", + options, + &run, + NULL); + if (GNUNET_SYSERR == ret) + return 3; + if (GNUNET_NO == ret) + return 0; + return global_ret; +} + + +/* end of anastasis-cli-redux.c */ diff --git a/src/cli/resources/00-backup.json b/src/cli/resources/00-backup.json new file mode 100644 index 0000000..6e6c320 --- /dev/null +++ b/src/cli/resources/00-backup.json @@ -0,0 +1,8 @@ +{ + "continents": [ + "Europe", + "North America", + "Testcontinent" + ], + "backup_state": "CONTINENT_SELECTING" +}
\ No newline at end of file diff --git a/src/cli/resources/00-recovery.json b/src/cli/resources/00-recovery.json new file mode 100644 index 0000000..acff19a --- /dev/null +++ b/src/cli/resources/00-recovery.json @@ -0,0 +1,8 @@ +{ + "continents": [ + "Europe", + "North America", + "Testcontinent" + ], + "recovery_state": "CONTINENT_SELECTING" +}
\ No newline at end of file diff --git a/src/cli/resources/01-backup.json b/src/cli/resources/01-backup.json new file mode 100644 index 0000000..842d3af --- /dev/null +++ b/src/cli/resources/01-backup.json @@ -0,0 +1,41 @@ +{ + "continents": [ + "Europe", + "North America", + "Testcontinent" + ], + "backup_state": "COUNTRY_SELECTING", + "selected_continent": "Testcontinent", + "countries": [ + { + "code": "xx", + "name": "Testland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Testlandt", + "de_CH": "Testlandi", + "fr": "Testpais", + "en": "Testland" + }, + "currency": "TESTKUDOS" + }, + { + "code": "xy", + "name": "Demoland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Demolandt", + "de_CH": "Demolandi", + "fr": "Demopais", + "en": "Demoland" + }, + "currency": "KUDOS" + } + ] +}
\ No newline at end of file diff --git a/src/cli/resources/01-recovery.json b/src/cli/resources/01-recovery.json new file mode 100644 index 0000000..11aafd3 --- /dev/null +++ b/src/cli/resources/01-recovery.json @@ -0,0 +1,41 @@ +{ + "continents": [ + "Europe", + "North America", + "Testcontinent" + ], + "recovery_state": "COUNTRY_SELECTING", + "selected_continent": "Testcontinent", + "countries": [ + { + "code": "xx", + "name": "Testland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Testlandt", + "de_CH": "Testlandi", + "fr": "Testpais", + "en": "Testland" + }, + "currency": "TESTKUDOS" + }, + { + "code": "xy", + "name": "Demoland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Demolandt", + "de_CH": "Demolandi", + "fr": "Demopais", + "en": "Demoland" + }, + "currency": "KUDOS" + } + ] +}
\ No newline at end of file diff --git a/src/cli/resources/02-backup.json b/src/cli/resources/02-backup.json new file mode 100644 index 0000000..c9bba16 --- /dev/null +++ b/src/cli/resources/02-backup.json @@ -0,0 +1,83 @@ +{ + "continents": [ + "Europe", + "North America", + "Testcontinent" + ], + "backup_state": "USER_ATTRIBUTES_COLLECTING", + "selected_continent": "Testcontinent", + "countries": [ + { + "code": "xx", + "name": "Testland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Testlandt", + "de_CH": "Testlandi", + "fr": "Testpais", + "en": "Testland" + }, + "currency": "TESTKUDOS" + }, + { + "code": "xy", + "name": "Demoland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Demolandt", + "de_CH": "Demolandi", + "fr": "Demopais", + "en": "Demoland" + }, + "currency": "KUDOS" + } + ], + "authentication_providers": { + "http://localhost:8086/": {}, + "http://localhost:8087/": {}, + "http://localhost:8088/": {}, + "http://localhost:8089/": {} + }, + "selected_country": "xx", + "currencies": [ "TESTKUDOS" ], + "required_attributes": [ + { + "type": "string", + "name": "full_name", + "label": "Full name", + "label_i18n": { + "de_DE": "Vollstaendiger Name", + "de_CH": "Vollstaendiger Name" + }, + "widget": "anastasis_gtk_ia_full_name" + }, + { + "type": "date", + "name": "birthdate", + "label": "Birthdate", + "label_i18n": { + "de_CH": "Geburtsdatum" + }, + "widget": "anastasis_gtk_ia_birthdate" + }, + { + "type": "string", + "name": "sq_number", + "label": "Square number", + "label_i18n":{ + "de_DE":"Quadratzahl", + "de_CH":"Quadratzahl" + }, + "widget": "anastasis_gtk_xx_square", + "uuid" : "ed790bca-89bf-11eb-96f2-233996cf644e", + "validation-regex": "^[0-9]+$", + "validation-logic": "XX_SQUARE_check" + } + ] +} diff --git a/src/cli/resources/02-recovery.json b/src/cli/resources/02-recovery.json new file mode 100644 index 0000000..79cfd6d --- /dev/null +++ b/src/cli/resources/02-recovery.json @@ -0,0 +1,83 @@ +{ + "continents": [ + "Europe", + "North America", + "Testcontinent" + ], + "recovery_state": "USER_ATTRIBUTES_COLLECTING", + "selected_continent": "Testcontinent", + "countries": [ + { + "code": "xx", + "name": "Testland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Testlandt", + "de_CH": "Testlandi", + "fr": "Testpais", + "en": "Testland" + }, + "currency": "TESTKUDOS" + }, + { + "code": "xy", + "name": "Demoland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Demolandt", + "de_CH": "Demolandi", + "fr": "Demopais", + "en": "Demoland" + }, + "currency": "KUDOS" + } + ], + "authentication_providers": { + "http://localhost:8086/": {}, + "http://localhost:8087/": {}, + "http://localhost:8088/": {}, + "http://localhost:8089/": {} + }, + "selected_country": "xx", + "currencies": [ "TESTKUDOS" ], + "required_attributes": [ + { + "type": "string", + "name": "full_name", + "label": "Full name", + "label_i18n": { + "de_DE": "Vollstaendiger Name", + "de_CH": "Vollstaendiger Name" + }, + "widget": "anastasis_gtk_ia_full_name" + }, + { + "type": "date", + "name": "birthdate", + "label": "Birthdate", + "label_i18n": { + "de_CH": "Geburtsdatum" + }, + "widget": "anastasis_gtk_ia_birthdate" + }, + { + "type": "string", + "name": "sq_number", + "label": "Square number", + "label_i18n":{ + "de_DE":"Quadratzahl", + "de_CH":"Quadratzahl" + }, + "widget": "anastasis_gtk_xx_square", + "uuid" : "ed790bca-89bf-11eb-96f2-233996cf644e", + "validation-regex": "^[0-9]+$", + "validation-logic": "XX_SQUARE_check" + } + ] +} diff --git a/src/cli/resources/03-backup.json b/src/cli/resources/03-backup.json new file mode 100644 index 0000000..9d599d7 --- /dev/null +++ b/src/cli/resources/03-backup.json @@ -0,0 +1,155 @@ +{ + "continents": [ + "Europe", + "North America", + "Testcontinent" + ], + "backup_state": "AUTHENTICATIONS_EDITING", + "selected_continent": "Testcontinent", + "countries": [ + { + "code": "xx", + "name": "Testland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Testlandt", + "de_CH": "Testlandi", + "fr": "Testpais", + "en": "Testland" + }, + "currency": "TESTKUDOS" + }, + { + "code": "xy", + "name": "Demoland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Demolandt", + "de_CH": "Demolandi", + "fr": "Demopais", + "en": "Demoland" + }, + "currency": "KUDOS" + } + ], + "authentication_providers": { + "http://localhost:8086/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #1 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "F0HEYJQW81ZAZ3VYMZHFG8T1Z0" + }, + "http://localhost:8087/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #2 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "D378FWXHJB8JHPQFQRZGGV9PWG" + }, + "http://localhost:8088/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #3 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "7W9W4A4TTWSWRPJ76RNDPJHSPR" + }, + "http://localhost:8089/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #4 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "PN0VJF6KDSBYN40SGRCEXPB07M" + } + }, + "selected_country": "xx", + "currencies": ["TESTKUDOS"], + "required_attributes": [ + { + "type": "string", + "name": "full_name", + "label": "Full name", + "label_i18n": { + "de_DE": "Vollstaendiger Name", + "de_CH": "Vollstaendiger Name" + }, + "widget": "anastasis_gtk_ia_full_name" + }, + { + "type": "date", + "name": "birthdate", + "label": "Birthdate", + "label_i18n": { + "de_CH": "Geburtsdatum" + }, + "widget": "anastasis_gtk_ia_birthdate" + }, + { + "type": "string", + "name": "ahv_number", + "label": "AHV number", + "label_i18n": { + "de_DE": "AHV-Nummer", + "de_CH": "AHV-Nummer" + }, + "widget": "anastasis_gtk_ia_ahv", + "validation-regex": "^(756).[0-9]{4}.[0-9]{4}.[0-9]{2}|(756)[0-9]{10}$", + "validation-logic": "CH_AVH_check" + } + ], + "identity_attributes": { + "full_name": "Max Musterman", + "sq_number": 4, + "birthdate": "2000-01-01" + } +} diff --git a/src/cli/resources/04-backup.json b/src/cli/resources/04-backup.json new file mode 100644 index 0000000..15c329a --- /dev/null +++ b/src/cli/resources/04-backup.json @@ -0,0 +1,172 @@ +{ + "continents": [ + "Europe", + "North America", + "Testcontinent" + ], + "backup_state": "AUTHENTICATIONS_EDITING", + "selected_continent": "Testcontinent", + "countries": [ + { + "code": "xx", + "name": "Testland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Testlandt", + "de_CH": "Testlandi", + "fr": "Testpais", + "en": "Testland" + }, + "currency": "TESTKUDOS" + }, + { + "code": "xy", + "name": "Demoland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Demolandt", + "de_CH": "Demolandi", + "fr": "Demopais", + "en": "Demoland" + }, + "currency": "KUDOS" + } + ], + "authentication_providers": { + "http://localhost:8086/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #1 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "F0HEYJQW81ZAZ3VYMZHFG8T1Z0" + }, + "http://localhost:8087/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #2 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "D378FWXHJB8JHPQFQRZGGV9PWG" + }, + "http://localhost:8088/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #3 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "7W9W4A4TTWSWRPJ76RNDPJHSPR" + }, + "http://localhost:8089/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #4 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "PN0VJF6KDSBYN40SGRCEXPB07M" + } + }, + "selected_country": "xx", + "currencies": [ "TESTKUDOS" ], + "required_attributes": [ + { + "type": "string", + "name": "full_name", + "label": "Full name", + "label_i18n": { + "de_DE": "Vollstaendiger Name", + "de_CH": "Vollstaendiger Name" + }, + "widget": "anastasis_gtk_ia_full_name" + }, + { + "type": "date", + "name": "birthdate", + "label": "Birthdate", + "label_i18n": { + "de_CH": "Geburtsdatum" + }, + "widget": "anastasis_gtk_ia_birthdate" + }, + { + "type": "string", + "name": "ahv_number", + "label": "AHV number", + "label_i18n": { + "de_DE": "AHV-Nummer", + "de_CH": "AHV-Nummer" + }, + "widget": "anastasis_gtk_ia_ahv", + "validation-regex": "^(756).[0-9]{4}.[0-9]{4}.[0-9]{2}|(756)[0-9]{10}$", + "validation-logic": "CH_AVH_check" + } + ], + "identity_attributes": { + "full_name": "Max Musterman", + "sq_number": 4, + "birthdate": "2000-01-01" + }, + "authentication_methods": [ + { + "type": "question", + "instructions": "What's your name?", + "challenge": "Hans" + }, + { + "type": "question", + "instructions": "What's your X name?", + "challenge": "Hansx" + }, + { + "type": "question", + "instructions": "Where do you live?", + "challenge": "Mars" + } + ] +} diff --git a/src/cli/resources/05-backup.json b/src/cli/resources/05-backup.json new file mode 100644 index 0000000..c0ce8ae --- /dev/null +++ b/src/cli/resources/05-backup.json @@ -0,0 +1,213 @@ +{ + "continents": [ + "Europe", + "North America", + "Testcontinent" + ], + "backup_state": "POLICIES_REVIEWING", + "selected_continent": "Testcontinent", + "countries": [ + { + "code": "xx", + "name": "Testland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Testlandt", + "de_CH": "Testlandi", + "fr": "Testpais", + "en": "Testland" + }, + "currency": "TESTKUDOS" + }, + { + "code": "xy", + "name": "Demoland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Demolandt", + "de_CH": "Demolandi", + "fr": "Demopais", + "en": "Demoland" + }, + "currency": "KUDOS" + } + ], + "authentication_providers": { + "http://localhost:8086/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #1 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "F0HEYJQW81ZAZ3VYMZHFG8T1Z0" + }, + "http://localhost:8087/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #2 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "D378FWXHJB8JHPQFQRZGGV9PWG" + }, + "http://localhost:8088/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #3 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "7W9W4A4TTWSWRPJ76RNDPJHSPR" + }, + "http://localhost:8089/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #4 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "PN0VJF6KDSBYN40SGRCEXPB07M" + } + }, + "selected_country": "xx", + "currencies": [ "TESTKUDOS" ], + "required_attributes": [ + { + "type": "string", + "name": "full_name", + "label": "Full name", + "label_i18n": { + "de_DE": "Vollstaendiger Name", + "de_CH": "Vollstaendiger Name" + }, + "widget": "anastasis_gtk_ia_full_name" + }, + { + "type": "date", + "name": "birthdate", + "label": "Birthdate", + "label_i18n": { + "de_CH": "Geburtsdatum" + }, + "widget": "anastasis_gtk_ia_birthdate" + }, + { + "type": "string", + "name": "ahv_number", + "label": "AHV number", + "label_i18n": { + "de_DE": "AHV-Nummer", + "de_CH": "AHV-Nummer" + }, + "widget": "anastasis_gtk_ia_ahv", + "validation-regex": "^(756).[0-9]{4}.[0-9]{4}.[0-9]{2}|(756)[0-9]{10}$", + "validation-logic": "CH_AVH_check" + } + ], + "identity_attributes": { + "full_name": "Max Musterman", + "sq_number": 4, + "birthdate": "2000-01-01" + }, + "authentication_methods": [ + { + "type": "question", + "instructions": "What's your name?", + "challenge": "Hans" + }, + { + "type": "question", + "instructions": "What's your X name?", + "challenge": "Hansx" + }, + { + "type": "question", + "instructions": "Where do you live?", + "challenge": "Mars" + } + ], + "policies": [ + { + "recovery_cost": "TESTKUDOS:0", + "methods": [ + { + "authentication_method": 0, + "provider": "http://localhost:8089/" + }, + { + "authentication_method": 1, + "provider": "http://localhost:8088/" + } + ] + }, + { + "recovery_cost": "TESTKUDOS:0", + "methods": [ + { + "authentication_method": 0, + "provider": "http://localhost:8089/" + }, + { + "authentication_method": 2, + "provider": "http://localhost:8088/" + } + ] + }, + { + "recovery_cost": "TESTKUDOS:0", + "methods": [ + { + "authentication_method": 1, + "provider": "http://localhost:8089/" + }, + { + "authentication_method": 2, + "provider": "http://localhost:8088/" + } + ] + } + ] +} diff --git a/src/cli/resources/06-backup.json b/src/cli/resources/06-backup.json new file mode 100644 index 0000000..d1f0b9e --- /dev/null +++ b/src/cli/resources/06-backup.json @@ -0,0 +1,223 @@ +{ + "continents": [ + "Europe", + "North America", + "Testcontinent" + ], + "backup_state": "SECRET_EDITING", + "selected_continent": "Testcontinent", + "countries": [ + { + "code": "xx", + "name": "Testland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Testlandt", + "de_CH": "Testlandi", + "fr": "Testpais", + "en": "Testland" + }, + "currency": "TESTKUDOS" + }, + { + "code": "xy", + "name": "Demoland", + "continent": "Testcontinent", + "continent_i18n": { + "xx": "Testkontinent" + }, + "name_i18n": { + "de_DE": "Demolandt", + "de_CH": "Demolandi", + "fr": "Demopais", + "en": "Demoland" + }, + "currency": "KUDOS" + } + ], + "authentication_providers": { + "http://localhost:8086/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #1 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "F0HEYJQW81ZAZ3VYMZHFG8T1Z0" + }, + "http://localhost:8087/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #2 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "D378FWXHJB8JHPQFQRZGGV9PWG" + }, + "http://localhost:8088/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #3 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "7W9W4A4TTWSWRPJ76RNDPJHSPR" + }, + "http://localhost:8089/": { + "methods": [ + { + "type": "question", + "usage_fee": "TESTKUDOS:0" + } + ], + "annual_fee": "TESTKUDOS:4.99", + "truth_upload_fee": "TESTKUDOS:0.01", + "liability_limit": "TESTKUDOS:1", + "truth_lifetime": { + "d_ms": 63115200000 + }, + "currency": "TESTKUDOS", + "business_name": "Data loss #4 Inc.", + "storage_limit_in_megabytes": 1, + "salt": "PN0VJF6KDSBYN40SGRCEXPB07M" + } + }, + "selected_country": "xx", + "currencies": ["TESTKUDOS"], + "required_attributes": [ + { + "type": "string", + "name": "full_name", + "label": "Full name", + "label_i18n": { + "de_DE": "Vollstaendiger Name", + "de_CH": "Vollstaendiger Name" + }, + "widget": "anastasis_gtk_ia_full_name" + }, + { + "type": "date", + "name": "birthdate", + "label": "Birthdate", + "label_i18n": { + "de_CH": "Geburtsdatum" + }, + "widget": "anastasis_gtk_ia_birthdate" + }, + { + "type": "string", + "name": "ahv_number", + "label": "AHV number", + "label_i18n": { + "de_DE": "AHV-Nummer", + "de_CH": "AHV-Nummer" + }, + "widget": "anastasis_gtk_ia_ahv", + "validation-regex": "^(756).[0-9]{4}.[0-9]{4}.[0-9]{2}|(756)[0-9]{10}$", + "validation-logic": "CH_AVH_check" + } + ], + "identity_attributes": { + "full_name": "Max Musterman", + "ahv_number": "756.9217.0769.85", + "birth_year": 2000, + "birth_month": 1, + "birth_day": 1 + }, + "authentication_methods": [ + { + "type": "question", + "instructions": "What's your name?", + "challenge": "Hans" + }, + { + "type": "question", + "instructions": "What's your X name?", + "challenge": "Hansx" + }, + { + "type": "question", + "instructions": "Where do you live?", + "challenge": "Mars" + } + ], + "policy_providers" : [ + { + "provider_url": "http://localhost:8089/" + }, + { + "provider_url": "http://localhost:8089/" + } + ], + "policies": [ + { + "recovery_cost": "TESTKUDOS:0", + "methods": [ + { + "authentication_method": 0, + "provider": "http://localhost:8089/" + }, + { + "authentication_method": 1, + "provider": "http://localhost:8088/" + } + ] + }, + { + "recovery_cost": "TESTKUDOS:0", + "methods": [ + { + "authentication_method": 0, + "provider": "http://localhost:8089/" + }, + { + "authentication_method": 2, + "provider": "http://localhost:8088/" + } + ] + }, + { + "recovery_cost": "TESTKUDOS:0", + "methods": [ + { + "authentication_method": 1, + "provider": "http://localhost:8089/" + }, + { + "authentication_method": 2, + "provider": "http://localhost:8088/" + } + ] + } + ] +} diff --git a/src/cli/test_anastasis_reducer_1.conf b/src/cli/test_anastasis_reducer_1.conf new file mode 100644 index 0000000..6a9704d --- /dev/null +++ b/src/cli/test_anastasis_reducer_1.conf @@ -0,0 +1,9 @@ +@INLINE@ test_reducer.conf + +[anastasis] +PORT = 8086 +SERVER_SALT = AUfO1KGOKYIFlFQg +BUSINESS_NAME = "Data loss #1 Inc." + +[stasis-postgres] +CONFIG = postgres:///anastasischeck1 diff --git a/src/cli/test_anastasis_reducer_2.conf b/src/cli/test_anastasis_reducer_2.conf new file mode 100644 index 0000000..f909ade --- /dev/null +++ b/src/cli/test_anastasis_reducer_2.conf @@ -0,0 +1,9 @@ +@INLINE@ test_reducer.conf + +[anastasis] +PORT = 8087 +SERVER_SALT = BUfO1KGOKYIFlFQg +BUSINESS_NAME = "Data loss #2 Inc." + +[stasis-postgres] +CONFIG = postgres:///anastasischeck2 diff --git a/src/cli/test_anastasis_reducer_3.conf b/src/cli/test_anastasis_reducer_3.conf new file mode 100644 index 0000000..63c38ff --- /dev/null +++ b/src/cli/test_anastasis_reducer_3.conf @@ -0,0 +1,9 @@ +@INLINE@ test_reducer.conf + +[anastasis] +PORT = 8088 +SERVER_SALT = CUfO1KGOKYIFlFQg +BUSINESS_NAME = "Data loss #3 Inc." + +[stasis-postgres] +CONFIG = postgres:///anastasischeck3 diff --git a/src/cli/test_anastasis_reducer_4.conf b/src/cli/test_anastasis_reducer_4.conf new file mode 100644 index 0000000..a6d590e --- /dev/null +++ b/src/cli/test_anastasis_reducer_4.conf @@ -0,0 +1,9 @@ +@INLINE@ test_reducer.conf + +[anastasis] +PORT = 8089 +SERVER_SALT = DUfO1KGOKYIFlFQg +BUSINESS_NAME = "Data loss #4 Inc." + +[stasis-postgres] +CONFIG = postgres:///anastasischeck4 diff --git a/src/cli/test_anastasis_reducer_add_authentication.sh b/src/cli/test_anastasis_reducer_add_authentication.sh new file mode 100755 index 0000000..7d69076 --- /dev/null +++ b/src/cli/test_anastasis_reducer_add_authentication.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " ERROR: $1" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + for n in `jobs -p` + do + kill $n 2> /dev/null || true + done + rm -f $TFILE $SFILE + wait +} + +SFILE=`mktemp test_reducer_stateXXXXXX` +TFILE=`mktemp test_reducer_stateXXXXXX` + +# Install cleanup handler (except for kill -9) +trap cleanup EXIT + +# Check we can actually run +echo -n "Testing for jq" +jq -h > /dev/null || exit_skip "jq required" +echo " FOUND" + +echo -n "Testing for anastasis-reducer ..." +anastasis-reducer -h > /dev/null || exit_skip "anastasis-reducer required" +echo " FOUND" + +echo -n "Test add authentication ..." + +# First method +anastasis-reducer -a \ + '{"authentication_method": { + "type": "question", + "instructions": "What is your name?", + "challenge": "91GPWWR" + } }' \ + add_authentication resources/03-backup.json $TFILE + +STATE=`jq -r -e .backup_state < $TFILE` +if test "$STATE" != "AUTHENTICATIONS_EDITING" +then + exit_fail "Expected new state to be 'AUTHENTICATIONS_EDITING', got '$STATE'" +fi + +ARRAY_LENGTH=`jq -r -e '.authentication_methods | length' < $TFILE` +if test $ARRAY_LENGTH != 1 +then + exit_fail "Expected array length to be 1, got '$ARRAY_LENGTH'" +fi + +echo -n "." +# Second method +anastasis-reducer -a \ + '{"authentication_method": { + "type": "question", + "instructions": "How old are you?", + "challenge": "64S36" + }}' \ + add_authentication $TFILE $SFILE + +STATE=`jq -r -e .backup_state < $SFILE` +if test "$STATE" != "AUTHENTICATIONS_EDITING" +then + exit_fail "Expected new state to be 'AUTHENTICATIONS_EDITING', got '$STATE'" +fi + +ARRAY_LENGTH=`jq -r -e '.authentication_methods | length' < $SFILE` +if test $ARRAY_LENGTH != 2 +then + exit_fail "Expected array length to be 2, got '$ARRAY_LENGTH'" +fi + +echo -n "." + +# Third method +anastasis-reducer -a \ + '{"authentication_method": { + "type": "question", + "instructions": "Where do you live?", + "challenge": "9NGQ4WR" + }}' \ + add_authentication $SFILE $TFILE + +STATE=`jq -r -e .backup_state < $TFILE` +if test "$STATE" != "AUTHENTICATIONS_EDITING" +then + exit_fail "Expected new state to be 'AUTHENTICATIONS_EDITING', got '$STATE'" +fi + +ARRAY_LENGTH=`jq -r -e '.authentication_methods | length' < $TFILE` +if test $ARRAY_LENGTH != 3 +then + exit_fail "Expected array length to be 3, got '$ARRAY_LENGTH'" +fi + +echo " OK" + + +echo -n "Test delete authentication ..." + +anastasis-reducer -a \ + '{"authentication_method": 2 }' \ + delete_authentication $TFILE $SFILE + +STATE=`jq -r -e .backup_state < $SFILE` +if test "$STATE" != "AUTHENTICATIONS_EDITING" +then + exit_fail "Expected new state to be 'AUTHENTICATIONS_EDITING', got '$STATE'" +fi + +ARRAY_LENGTH=`jq -r -e '.authentication_methods | length' < $SFILE` +if test $ARRAY_LENGTH != 2 +then + exit_fail "Expected array length to be 2, got '$ARRAY_LENGTH'" +fi + +echo " OK" + +exit 0 diff --git a/src/cli/test_anastasis_reducer_backup_enter_user_attributes.sh b/src/cli/test_anastasis_reducer_backup_enter_user_attributes.sh new file mode 100755 index 0000000..433438e --- /dev/null +++ b/src/cli/test_anastasis_reducer_backup_enter_user_attributes.sh @@ -0,0 +1,140 @@ +#!/bin/bash + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " ERROR: $1" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + for n in `jobs -p` + do + kill $n 2> /dev/null || true + done + wait +} + +CONF_1="test_anastasis_reducer_1.conf" +CONF_2="test_anastasis_reducer_2.conf" +CONF_3="test_anastasis_reducer_3.conf" +CONF_4="test_anastasis_reducer_4.conf" +TFILE=`mktemp test_reducer_stateXXXXXX` + +# Install cleanup handler (except for kill -9) +trap cleanup EXIT + +# Check we can actually run +echo -n "Testing for jq" +jq -h > /dev/null || exit_skip "jq required" +echo " FOUND" + +echo -n "Testing for anastasis-reducer ..." +anastasis-reducer -h > /dev/null || exit_skip "anastasis-reducer required" +echo " FOUND" + +echo -n "Testing for anastasis-httpd" +anastasis-httpd -h >/dev/null </dev/null || exit_skip " MISSING" +echo " FOUND" + + +# Name of the Postgres database we will use for the script. +# Will be dropped, do NOT use anything that might be used +# elsewhere +TARGET_DB_1=`anastasis-config -c $CONF_1 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` +TARGET_DB_2=`anastasis-config -c $CONF_2 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` +TARGET_DB_3=`anastasis-config -c $CONF_3 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` +TARGET_DB_4=`anastasis-config -c $CONF_4 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` + +echo -n "Initialize anastasis database ..." +dropdb $TARGET_DB_1 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_1 || exit_skip "Could not create database $TARGET_DB_1" +anastasis-dbinit -c $CONF_1 2> anastasis-dbinit_1.log +dropdb $TARGET_DB_2 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_2 || exit_skip "Could not create database $TARGET_DB_2" +anastasis-dbinit -c $CONF_2 2> anastasis-dbinit_2.log +dropdb $TARGET_DB_3 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_3 || exit_skip "Could not create database $TARGET_DB_3" +anastasis-dbinit -c $CONF_3 2> anastasis-dbinit_3.log +dropdb $TARGET_DB_4 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_4 || exit_skip "Could not create database $TARGET_DB_4" +anastasis-dbinit -c $CONF_4 2> anastasis-dbinit_4.log + +echo " OK" + +echo -n "Launching anastasis service ..." +anastasis-httpd -c $CONF_1 2> anastasis-httpd_1.log & +anastasis-httpd -c $CONF_2 2> anastasis-httpd_2.log & +anastasis-httpd -c $CONF_3 2> anastasis-httpd_3.log & +anastasis-httpd -c $CONF_4 2> anastasis-httpd_4.log & + +# Wait for anastasis service to be available +for n in `seq 1 50` +do + echo -n "." + sleep 0.1 + OK=0 + # anastasis_01 + wget http://localhost:8086/ -o /dev/null -O /dev/null >/dev/null || continue + # anastasis_02 + wget http://localhost:8087/ -o /dev/null -O /dev/null >/dev/null || continue + # anastasis_03 + wget http://localhost:8088/ -o /dev/null -O /dev/null >/dev/null || continue + # anastasis_04 + wget http://localhost:8089/ -o /dev/null -O /dev/null >/dev/null || continue + OK=1 + break +done + +if [ 1 != $OK ] +then + exit_skip "Failed to launch anastasis services" +fi +echo " OK" + +# Test user attributes collection in a backup state +echo -n "Test user attributes collection in a backup state ..." + +anastasis-reducer -L WARNING -a \ + '{"identity_attributes": { + "full_name": "Max Musterman", + "sq_number": "4", + "birthdate": "2000-01-01"}}' \ + enter_user_attributes resources/02-backup.json $TFILE + +STATE=`jq -r -e .backup_state < $TFILE` +if test "$STATE" != "AUTHENTICATIONS_EDITING" +then + exit_fail "Expected new state to be 'AUTHENTICATIONS_EDITING', got '$STATE'" +fi + +SELECTED_COUNTRY=`jq -r -e .selected_country < $TFILE` +if test "$SELECTED_COUNTRY" != "xx" +then + exit_fail "Expected selected country to be 'xx', got '$SELECTED_COUNTRY'" +fi + +echo "OK" + +echo -n "Test user attributes collection in a recovery state ..." +anastasis-reducer -a \ + '{"identity_attributes": { + "full_name": "Max Musterman", + "sq_number": "4", + "birthdate": "2000-01-01"}}' \ + enter_user_attributes resources/02-recovery.json $TFILE 2> /dev/null && exit_fail "Expected recovery to fail due to lacking policy data" + +echo "OK" + +rm -f $TFILE + +exit 0 diff --git a/src/cli/test_anastasis_reducer_done_authentication.sh b/src/cli/test_anastasis_reducer_done_authentication.sh new file mode 100755 index 0000000..87c738c --- /dev/null +++ b/src/cli/test_anastasis_reducer_done_authentication.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " ERROR: $1" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + for n in `jobs -p` + do + kill $n 2> /dev/null || true + done + rm -f $TFILE + wait +} + +# Install cleanup handler (except for kill -9) +TFILE=`mktemp test_reducer_stateXXXXXX` +trap cleanup EXIT + +# Check we can actually run +echo -n "Testing for jq ..." +jq -h > /dev/null || exit_skip "jq required" +echo " FOUND" + +echo -n "Testing for anastasis-reducer ..." +anastasis-reducer -h > /dev/null || exit_skip "anastasis-reducer required" +echo " FOUND" + + +echo -n "Test failing done authentication (next) ..." +anastasis-reducer next resources/03-backup.json $TFILE 2> /dev/null && exit_fail "Should have failed without challenges" + +echo " OK" + + +echo -n "Test done authentication (next) ..." +anastasis-reducer next resources/04-backup.json $TFILE + +STATE=`jq -r -e .backup_state < $TFILE` +if test "$STATE" != "POLICIES_REVIEWING" +then + exit_fail "Expected new state to be AUTHENTICATIONS_EDITING, got $STATE" +fi + +ARRAY_LENGTH=`jq -r -e '.policies | length' < $TFILE` +if test $ARRAY_LENGTH -lt 3 +then + exit_fail "Expected policy array length to be >= 3, got $ARRAY_LENGTH" +fi + +echo " OK" + +exit 0 diff --git a/src/cli/test_anastasis_reducer_done_policy_review.sh b/src/cli/test_anastasis_reducer_done_policy_review.sh new file mode 100755 index 0000000..7052067 --- /dev/null +++ b/src/cli/test_anastasis_reducer_done_policy_review.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " ERROR: $1" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + for n in `jobs -p` + do + kill $n 2> /dev/null || true + done + rm -f $TFILE + wait +} + +# Install cleanup handler (except for kill -9) +TFILE=`mktemp test_reducer_stateXXXXXX` +trap cleanup EXIT + + +# Check we can actually run +echo -n "Testing for jq ..." +jq -h > /dev/null || exit_skip "jq required" +echo " FOUND" + +echo -n "Testing for anastasis-reducer ..." +anastasis-reducer -h > /dev/null || exit_skip "anastasis-reducer required" +echo " FOUND" + +echo -n "Test done policy review (next) in a backup state ..." +anastasis-reducer next resources/05-backup.json $TFILE + +STATE=`jq -r -e .backup_state < $TFILE` +if test "$STATE" != "SECRET_EDITING" +then + exit_fail "Expected new state to be 'SECRET_EDITING', got '$STATE'" +fi + +ARRAY_LENGTH=`jq -r -e '.authentication_methods | length' < $TFILE` +if test $ARRAY_LENGTH -lt 3 +then + exit_fail "Expected auth methods array length to be >= 3, got $ARRAY_LENGTH" +fi + +ARRAY_LENGTH=`jq -r -e '.policies | length' < $TFILE` +if test $ARRAY_LENGTH -lt 3 +then + exit_fail "Expected policies array length to be >= 3, got $ARRAY_LENGTH" +fi + +echo " OK" + + + +echo -n "Test adding policy ..." +anastasis-reducer -a \ + '{ "policy" : [ + { "authentication_method" : 1, + "provider" : "http://localhost:8088/" }, + { "authentication_method" : 1, + "provider" : "http://localhost:8089/" } + ] }' \ + add_policy \ + resources/05-backup.json \ + $TFILE 2> /dev/null + +ARRAY_LENGTH=`jq -r -e '.policies | length' < $TFILE` +if test $ARRAY_LENGTH -lt 4 +then + exit_fail "Expected policy array length to be >= 4, got $ARRAY_LENGTH" +fi + +echo " OK" + + +echo -n "Test deleting policy ..." +anastasis-reducer -a \ + '{ "policy_index" : 2 }' \ + delete_policy \ + resources/05-backup.json \ + $TFILE 2> /dev/null + +ARRAY_LENGTH=`jq -r -e '.policies | length' < $TFILE` +if test $ARRAY_LENGTH -ge 3 +then + exit_fail "Expected policy array length to be < 3, got $ARRAY_LENGTH" +fi + +echo " OK" + + + +exit 0 diff --git a/src/cli/test_anastasis_reducer_enter_secret.sh b/src/cli/test_anastasis_reducer_enter_secret.sh new file mode 100755 index 0000000..dadd8d0 --- /dev/null +++ b/src/cli/test_anastasis_reducer_enter_secret.sh @@ -0,0 +1,417 @@ +#!/bin/bash +## Coloring style Text shell script +COLOR='\033[0;35m' +NOCOLOR='\033[0m' +BOLD="$(tput bold)" +NORM="$(tput sgr0)" + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " FAIL: $1" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + for n in `jobs -p` + do + kill $n 2> /dev/null || true + done + rm -rf $CONF $WALLET_DB $TFILE $UFILE $TMP_DIR + wait +} + +CONF_1="test_anastasis_reducer_1.conf" +CONF_2="test_anastasis_reducer_2.conf" +CONF_3="test_anastasis_reducer_3.conf" +CONF_4="test_anastasis_reducer_4.conf" + +# Exchange configuration file will be edited, so we create one +# from the template. +CONF=`mktemp test_reducerXXXXXX.conf` +cp test_reducer.conf $CONF + +TMP_DIR=`mktemp -d keys-tmp-XXXXXX` +WALLET_DB=`mktemp test_reducer_walletXXXXXX.json` +TFILE=`mktemp test_reducer_statePPXXXXXX` +UFILE=`mktemp test_reducer_stateBFXXXXXX` + +# Install cleanup handler (except for kill -9) +trap cleanup EXIT + +# Check we can actually run +echo -n "Testing for jq" +jq -h > /dev/null || exit_skip "jq required" +echo " FOUND" +echo -n "Testing for anastasis-reducer ..." +anastasis-reducer -h > /dev/null || exit_skip "anastasis-reducer required" +echo " FOUND" + +echo -n "Testing for taler" +taler-exchange-httpd -h > /dev/null || exit_skip " taler-exchange required" +taler-merchant-httpd -h > /dev/null || exit_skip " taler-merchant required" +echo " FOUND" + +echo -n "Testing for taler-bank-manage" +taler-bank-manage --help >/dev/null </dev/null || exit_skip " MISSING" +echo " FOUND" +echo -n "Testing for taler-wallet-cli" +taler-wallet-cli -v >/dev/null </dev/null || exit_skip " MISSING" +echo " FOUND" + +echo -n "Testing for anastasis-httpd" +anastasis-httpd -h >/dev/null </dev/null || exit_skip " MISSING" +echo " FOUND" + +echo -n "Initialize anastasis database ..." +# Name of the Postgres database we will use for the script. +# Will be dropped, do NOT use anything that might be used +# elsewhere +TARGET_DB_1=`anastasis-config -c $CONF_1 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` +TARGET_DB_2=`anastasis-config -c $CONF_2 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` +TARGET_DB_3=`anastasis-config -c $CONF_3 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` +TARGET_DB_4=`anastasis-config -c $CONF_4 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` + +dropdb $TARGET_DB_1 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_1 || exit_skip "Could not create database $TARGET_DB_1" +anastasis-dbinit -c $CONF_1 2> anastasis-dbinit_1.log +dropdb $TARGET_DB_2 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_2 || exit_skip "Could not create database $TARGET_DB_2" +anastasis-dbinit -c $CONF_2 2> anastasis-dbinit_2.log +dropdb $TARGET_DB_3 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_3 || exit_skip "Could not create database $TARGET_DB_3" +anastasis-dbinit -c $CONF_3 2> anastasis-dbinit_3.log +dropdb $TARGET_DB_4 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_4 || exit_skip "Could not create database $TARGET_DB_4" +anastasis-dbinit -c $CONF_4 2> anastasis-dbinit_4.log + +echo " OK" + +echo -n "Generating Taler auditor, exchange and merchant configurations ..." + +DATA_DIR=`taler-config -f -c $CONF -s PATHS -o TALER_HOME` +rm -rf $DATA_DIR + +# obtain key configuration data +MASTER_PRIV_FILE=`taler-config -f -c $CONF -s EXCHANGE -o MASTER_PRIV_FILE` +MASTER_PRIV_DIR=`dirname $MASTER_PRIV_FILE` +mkdir -p $MASTER_PRIV_DIR +gnunet-ecc -g1 $MASTER_PRIV_FILE > /dev/null 2> /dev/null +MASTER_PUB=`gnunet-ecc -p $MASTER_PRIV_FILE` +EXCHANGE_URL=`taler-config -c $CONF -s EXCHANGE -o BASE_URL` +MERCHANT_PORT=`taler-config -c $CONF -s MERCHANT -o PORT` +MERCHANT_URL=http://localhost:${MERCHANT_PORT}/ +BANK_PORT=`taler-config -c $CONF -s BANK -o HTTP_PORT` +BANK_URL=http://localhost:${BANK_PORT}/ +AUDITOR_URL=http://localhost:8083/ +AUDITOR_PRIV_FILE=`taler-config -f -c $CONF -s AUDITOR -o AUDITOR_PRIV_FILE` +AUDITOR_PRIV_DIR=`dirname $AUDITOR_PRIV_FILE` +mkdir -p $AUDITOR_PRIV_DIR +gnunet-ecc -g1 $AUDITOR_PRIV_FILE > /dev/null 2> /dev/null +AUDITOR_PUB=`gnunet-ecc -p $AUDITOR_PRIV_FILE` + +# patch configuration +TALER_DB=talercheck +taler-config -c $CONF -s exchange -o MASTER_PUBLIC_KEY -V $MASTER_PUB +taler-config -c $CONF -s merchant-exchange-default -o MASTER_KEY -V $MASTER_PUB +taler-config -c $CONF -s exchangedb-postgres -o CONFIG -V postgres:///$TALER_DB +taler-config -c $CONF -s auditordb-postgres -o CONFIG -V postgres:///$TALER_DB +taler-config -c $CONF -s merchantdb-postgres -o CONFIG -V postgres:///$TALER_DB +taler-config -c $CONF -s bank -o database -V postgres:///$TALER_DB +taler-config -c $CONF -s exchange -o KEYDIR -V "${TMP_DIR}/keydir/" +taler-config -c $CONF -s exchange -o REVOCATION_DIR -V "${TMP_DIR}/revdir/" + +echo " OK" + +echo -n "Setting up exchange ..." + +# reset database +dropdb $TALER_DB >/dev/null 2>/dev/null || true +createdb $TALER_DB || exit_skip "Could not create database $TALER_DB" +taler-exchange-dbinit -c $CONF +taler-merchant-dbinit -c $CONF +taler-auditor-dbinit -c $CONF +taler-auditor-exchange -c $CONF -m $MASTER_PUB -u $EXCHANGE_URL + +echo " OK" + +# Launch services +echo -n "Launching taler services ..." +taler-bank-manage-testing $CONF postgres:///$TALER_DB serve > taler-bank.log 2> taler-bank.err & +taler-exchange-secmod-eddsa -c $CONF 2> taler-exchange-secmod-eddsa.log & +taler-exchange-secmod-rsa -c $CONF 2> taler-exchange-secmod-rsa.log & +taler-exchange-httpd -c $CONF 2> taler-exchange-httpd.log & +taler-merchant-httpd -c $CONF -L INFO 2> taler-merchant-httpd.log & +taler-exchange-wirewatch -c $CONF 2> taler-exchange-wirewatch.log & +taler-auditor-httpd -L INFO -c $CONF 2> taler-auditor-httpd.log & + +echo " OK" + +echo -n "Launching anastasis services ..." +PREFIX="" #valgrind +$PREFIX anastasis-httpd -c $CONF_1 2> anastasis-httpd_1.log & +$PREFIX anastasis-httpd -c $CONF_2 2> anastasis-httpd_2.log & +$PREFIX anastasis-httpd -c $CONF_3 2> anastasis-httpd_3.log & +$PREFIX anastasis-httpd -c $CONF_4 2> anastasis-httpd_4.log & + +# Wait for bank to be available (usually the slowest) +for n in `seq 1 50` +do + echo -n "." + sleep 0.2 + OK=0 + # bank + wget --tries=1 --timeout=1 http://localhost:8082/ -o /dev/null -O /dev/null >/dev/null || continue + OK=1 + break +done + +if [ 1 != $OK ] +then + exit_skip "Failed to launch services (bank)" +fi + +# Wait for all other taler services to be available +for n in `seq 1 50` +do + echo -n "." + sleep 0.1 + OK=0 + # exchange + wget --tries=1 --timeout=1 http://localhost:8081/seed -o /dev/null -O /dev/null >/dev/null || continue + # merchant + wget --tries=1 --timeout=1 http://localhost:9966/ -o /dev/null -O /dev/null >/dev/null || continue + # auditor + wget --tries=1 --timeout=1 http://localhost:8083/ -o /dev/null -O /dev/null >/dev/null || continue + OK=1 + break +done + +if [ 1 != $OK ] +then + exit_skip "Failed to launch taler services" +fi + +echo "OK" + +echo -n "Setting up keys ..." +taler-exchange-offline -c $CONF \ + download \ + sign \ + enable-account payto://x-taler-bank/localhost/Exchange \ + enable-auditor $AUDITOR_PUB $AUDITOR_URL "TESTKUDOS Auditor" \ + wire-fee now x-taler-bank TESTKUDOS:0.01 TESTKUDOS:0.01 \ + upload &> taler-exchange-offline.log + +echo -n "." + +for n in `seq 1 3` +do + echo -n "." + OK=0 + wget --tries=1 --timeout=1 http://localhost:8081/keys -o /dev/null -O /dev/null >/dev/null || continue + OK=1 + break +done + +if [ 1 != $OK ] +then + exit_skip "Failed to setup keys" +fi + +echo " OK" + +echo -n "Setting up auditor signatures ..." +taler-auditor-offline -c $CONF \ + download sign upload &> taler-auditor-offline.log +echo " OK" + +echo -n "Waiting for anastasis services ..." + +# Wait for anastasis services to be available +for n in `seq 1 50` +do + echo -n "." + sleep 0.1 + OK=0 + # anastasis_01 + wget --tries=1 --timeout=1 http://localhost:8086/ -o /dev/null -O /dev/null >/dev/null || continue + # anastasis_02 + wget --tries=1 --timeout=1 http://localhost:8087/ -o /dev/null -O /dev/null >/dev/null || continue + # anastasis_03 + wget --tries=1 --timeout=1 http://localhost:8088/ -o /dev/null -O /dev/null >/dev/null || continue + # anastasis_04 + wget --tries=1 --timeout=1 http://localhost:8089/ -o /dev/null -O /dev/null >/dev/null || continue + OK=1 + break +done + +if [ 1 != $OK ] +then + exit_skip "Failed to launch anastasis services" +fi +echo "OK" + +echo -n "Configuring merchant instance ..." +# Setup merchant + +curl -H "Content-Type: application/json" -X POST -d '{"auth":{"method":"external"},"payto_uris":["payto://x-taler-bank/localhost/43"],"id":"default","name":"default","address":{},"jurisdiction":{},"default_max_wire_fee":"TESTKUDOS:1", "default_max_deposit_fee":"TESTKUDOS:1","default_wire_fee_amortization":1,"default_wire_transfer_delay":{"d_ms" : 3600000},"default_pay_delay":{"d_ms": 3600000}}' http://localhost:9966/private/instances + + +echo " DONE" + +echo -en $COLOR$BOLD"Test enter secret in a backup state ..."$NORM$NOCOLOR + +$PREFIX anastasis-reducer -a \ + '{"secret": { "value" : "veryhardtoguesssecret", "mime" : "text/plain" } }' \ + enter_secret resources/06-backup.json $TFILE + +STATE=`jq -r -e .backup_state < $TFILE` +if test "$STATE" != "SECRET_EDITING" +then + jq -e . $TFILE + exit_fail "Expected new state to be 'SECRET_EDITING', got '$STATE'" +fi + +echo " DONE" +echo -en $COLOR$BOLD"Test expiration change ..."$NORM$NOCOLOR + +MILLIS=`date '+%s'`000 +# Use 156 days into the future to get 1 year +MILLIS=`expr $MILLIS + 13478400000` + +$PREFIX anastasis-reducer -a \ + "$(jq -n ' + {"expiration": { "t_ms" : $MSEC } }' \ + --argjson MSEC $MILLIS + )" \ + update_expiration $TFILE $UFILE + +STATE=`jq -r -e .backup_state < $UFILE` +if test "$STATE" != "SECRET_EDITING" +then + jq -e . $UFILE + exit_fail "Expected new state to be 'SECRET_EDITING', got '$STATE'" +fi + +FEES=`jq -r -e '.upload_fees[0].fee' < $UFILE` +# 4x 4.99 for annual fees, plus 4x0.01 for truth uploads +if test "$FEES" != "TESTKUDOS:20" +then + jq -e . $TFILE + exit_fail "Expected upload fees to be 'TESTKUDOS:20', got '$FEES'" +fi + + +echo " DONE" +echo -en $COLOR$BOLD"Test advance to payment ..."$NORM$NOCOLOR + +$PREFIX anastasis-reducer next $UFILE $TFILE + +STATE=`jq -r -e .backup_state < $TFILE` +if test "$STATE" != "TRUTHS_PAYING" +then + jq -e . $TFILE + exit_fail "Expected new state to be 'TRUTHS_PAYING', got '$STATE'" +fi + +TMETHOD=`jq -r -e '.policies[0].methods[0].truth.type' < $TFILE` +if test $TMETHOD != "question" +then + exit_fail "Expected method to be >='question', got $TMETHOD" +fi + +echo " OK" +#Pay + +echo -en $COLOR$BOLD"Withdrawing amount to wallet ..."$NORM$NOCOLOR + +rm $WALLET_DB +taler-wallet-cli --no-throttle --wallet-db=$WALLET_DB api 'withdrawTestBalance' \ + "$(jq -n ' + { + amount: "TESTKUDOS:40", + bankBaseUrl: $BANK_URL, + exchangeBaseUrl: $EXCHANGE_URL + }' \ + --arg BANK_URL "$BANK_URL" \ + --arg EXCHANGE_URL "$EXCHANGE_URL" + )" 2>wallet.err >wallet.log +taler-wallet-cli --wallet-db=$WALLET_DB run-until-done 2>wallet.err >wallet.log + +echo " OK" + +echo -en $COLOR$BOLD"Making payments for truth uploads ... "$NORM$NOCOLOR +OBJECT_SIZE=`jq -r -e '.payments | length' < $TFILE` +for ((INDEX=0; INDEX < $OBJECT_SIZE; INDEX++)) +do + PAY_URI=`jq --argjson INDEX $INDEX -r -e '.payments[$INDEX]' < $TFILE` + # run wallet CLI + echo -n "$INDEX" + taler-wallet-cli --wallet-db=$WALLET_DB handle-uri $PAY_URI -y 2>wallet.err >wallet.log + echo -n "," +done +echo " OK" +echo -e $COLOR$BOLD"Running wallet run-pending..."$NORM$NOCOLOR +taler-wallet-cli --wallet-db=$WALLET_DB run-pending 2>wallet.err >wallet.log +echo -e $COLOR$BOLD"Payments done"$NORM$NOCOLOR + + +echo -en $COLOR$BOLD"Try to upload again ..."$NORM$NOCOLOR +$PREFIX anastasis-reducer pay $TFILE $UFILE +mv $UFILE $TFILE +echo " OK" + + +STATE=`jq -r -e .backup_state < $TFILE` +if test "$STATE" != "POLICIES_PAYING" +then + exit_fail "Expected new state to be 'POLICIES_PAYING', got '$STATE'" +fi + +export TFILE +export UFILE + +echo -en $COLOR$BOLD"Making payments for policy uploads ... "$NORM$NOCOLOR +OBJECT_SIZE=`jq -r -e '.policy_payment_requests | length' < $TFILE` +for ((INDEX=0; INDEX < $OBJECT_SIZE; INDEX++)) +do + PAY_URI=`jq --argjson INDEX $INDEX -r -e '.policy_payment_requests[$INDEX].payto' < $TFILE` + # run wallet CLI + export PAY_URI + echo -n "$INDEX" + taler-wallet-cli --wallet-db=$WALLET_DB handle-uri $PAY_URI -y 2>wallet.err >wallet.log + echo -n "," +done +echo " OK" +echo -e $COLOR$BOLD"Running wallet run-pending..."$NORM$NOCOLOR +taler-wallet-cli --wallet-db=$WALLET_DB run-pending 2>wallet.err >wallet.log +echo -e $COLOR$BOLD"Payments done"$NORM$NOCOLOR + +echo -en $COLOR$BOLD"Try to upload again ..."$NORM$NOCOLOR +$PREFIX anastasis-reducer pay $TFILE $UFILE + +echo " OK" + +echo -n "Final checks ..." + +STATE=`jq -r -e .backup_state < $UFILE` +if test "$STATE" != "BACKUP_FINISHED" +then + exit_fail "Expected new state to be BACKUP_FINISHED, got $STATE" +fi + +jq -r -e .core_secret < $UFILE > /dev/null && exit_fail "'core_secret' was not cleared upon success" + +echo " OK" + +exit 0 diff --git a/src/cli/test_anastasis_reducer_initialize_state.sh b/src/cli/test_anastasis_reducer_initialize_state.sh new file mode 100755 index 0000000..9dc0c59 --- /dev/null +++ b/src/cli/test_anastasis_reducer_initialize_state.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " FAIL: $1" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + for n in `jobs -p` + do + kill $n 2> /dev/null || true + done + rm -f $SFILE $TFILE + wait +} + +# Install cleanup handler (except for kill -9) +SFILE=`mktemp test_reducer_stateXXXXXX` +TFILE=`mktemp test_reducer_stateXXXXXX` + +trap cleanup EXIT + +# Check we can actually run +echo -n "Testing for jq ..." +jq -h > /dev/null || exit_skip "jq required" +echo " FOUND" +echo -n "Testing for anastasis-reducer ..." +anastasis-reducer -h > /dev/null || exit_skip "anastasis-reducer required" +echo " FOUND" +echo -n "Test initialization of a backup state ..." +anastasis-reducer -b $SFILE + +STATE=`jq -r -e .backup_state < $SFILE` +if test "$STATE" != "CONTINENT_SELECTING" +then + exit_fail "Expected initial state to be CONTINENT_SELECTING, got $STATE" +fi +jq -e .continents[0] < $SFILE > /dev/null || exit_fail "Expected initial state to include continents" + +echo " OK" + +echo -n "Test initialization of a recovery state ..." +anastasis-reducer -r $TFILE + +STATE=`jq -r -e .recovery_state < $TFILE` +if test "$STATE" != "CONTINENT_SELECTING" +then + exit_fail "Expected initial state to be CONTINENT_SELECTING, got $STATE" +fi +jq -e .continents[0] < $TFILE > /dev/null || exit_fail "Expected initial state to include continents" +echo " OK" + +exit 0 diff --git a/src/cli/test_anastasis_reducer_recovery_enter_user_attributes.sh b/src/cli/test_anastasis_reducer_recovery_enter_user_attributes.sh new file mode 100755 index 0000000..d0562e2 --- /dev/null +++ b/src/cli/test_anastasis_reducer_recovery_enter_user_attributes.sh @@ -0,0 +1,516 @@ +#!/bin/bash + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " FAIL: $1" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + for n in `jobs -p` + do + kill $n 2> /dev/null || true + done + rm -rf $CONF $WALLET_DB $R1FILE $R2FILE $B1FILE $B2FILE $TMP_DIR + wait +} + + +CONF_1="test_anastasis_reducer_1.conf" +CONF_2="test_anastasis_reducer_2.conf" +CONF_3="test_anastasis_reducer_3.conf" +CONF_4="test_anastasis_reducer_4.conf" + + +# Exchange configuration file will be edited, so we create one +# from the template. +CONF=`mktemp test_reducerXXXXXX.conf` +cp test_reducer.conf $CONF + +TMP_DIR=`mktemp -d keys-tmp-XXXXXX` +WALLET_DB=`mktemp test_reducer_walletXXXXXX.json` +B1FILE=`mktemp test_reducer_stateB1XXXXXX` +B2FILE=`mktemp test_reducer_stateB2XXXXXX` +R1FILE=`mktemp test_reducer_stateR1XXXXXX` +R2FILE=`mktemp test_reducer_stateR2XXXXXX` + +# Install cleanup handler (except for kill -9) +trap cleanup EXIT + +# Check we can actually run +echo -n "Testing for jq" +jq -h > /dev/null || exit_skip "jq required" +echo " FOUND" +echo -n "Testing for anastasis-reducer ..." +anastasis-reducer -h > /dev/null || exit_skip "anastasis-reducer required" +echo " FOUND" + +echo -n "Testing for taler" +taler-exchange-httpd -h > /dev/null || exit_skip " taler-exchange required" +taler-merchant-httpd -h > /dev/null || exit_skip " taler-merchant required" +echo " FOUND" + +echo -n "Testing for taler-bank-manage" +taler-bank-manage --help >/dev/null </dev/null || exit_skip " MISSING" +echo " FOUND" +echo -n "Testing for taler-wallet-cli" +taler-wallet-cli -v >/dev/null </dev/null || exit_skip " MISSING" +echo " FOUND" + +echo -n "Testing for anastasis-httpd" +anastasis-httpd -h >/dev/null </dev/null || exit_skip " MISSING" +echo " FOUND" + +echo -n "Initialize anastasis database ..." +# Name of the Postgres database we will use for the script. +# Will be dropped, do NOT use anything that might be used +# elsewhere +TARGET_DB_1=`anastasis-config -c $CONF_1 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` +TARGET_DB_2=`anastasis-config -c $CONF_2 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` +TARGET_DB_3=`anastasis-config -c $CONF_3 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` +TARGET_DB_4=`anastasis-config -c $CONF_4 -s stasis-postgres -o CONFIG | sed -e "s/^postgres:\/\/\///"` + +dropdb $TARGET_DB_1 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_1 || exit_skip "Could not create database $TARGET_DB_1" +anastasis-dbinit -c $CONF_1 2> anastasis-dbinit_1.log +dropdb $TARGET_DB_2 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_2 || exit_skip "Could not create database $TARGET_DB_2" +anastasis-dbinit -c $CONF_2 2> anastasis-dbinit_2.log +dropdb $TARGET_DB_3 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_3 || exit_skip "Could not create database $TARGET_DB_3" +anastasis-dbinit -c $CONF_3 2> anastasis-dbinit_3.log +dropdb $TARGET_DB_4 >/dev/null 2>/dev/null || true +createdb $TARGET_DB_4 || exit_skip "Could not create database $TARGET_DB_4" +anastasis-dbinit -c $CONF_4 2> anastasis-dbinit_4.log + +echo " OK" + +echo -n "Generating Taler auditor, exchange and merchant configurations ..." + +DATA_DIR=`taler-config -f -c $CONF -s PATHS -o TALER_HOME` +rm -rf $DATA_DIR + +# obtain key configuration data +MASTER_PRIV_FILE=`taler-config -f -c $CONF -s EXCHANGE -o MASTER_PRIV_FILE` +MASTER_PRIV_DIR=`dirname $MASTER_PRIV_FILE` +mkdir -p $MASTER_PRIV_DIR +gnunet-ecc -g1 $MASTER_PRIV_FILE > /dev/null 2> /dev/null +MASTER_PUB=`gnunet-ecc -p $MASTER_PRIV_FILE` +EXCHANGE_URL=`taler-config -c $CONF -s EXCHANGE -o BASE_URL` +MERCHANT_PORT=`taler-config -c $CONF -s MERCHANT -o PORT` +MERCHANT_URL=http://localhost:${MERCHANT_PORT}/ +BANK_PORT=`taler-config -c $CONF -s BANK -o HTTP_PORT` +BANK_URL=http://localhost:${BANK_PORT}/ +AUDITOR_URL=http://localhost:8083/ +AUDITOR_PRIV_FILE=`taler-config -f -c $CONF -s AUDITOR -o AUDITOR_PRIV_FILE` +AUDITOR_PRIV_DIR=`dirname $AUDITOR_PRIV_FILE` +mkdir -p $AUDITOR_PRIV_DIR +gnunet-ecc -g1 $AUDITOR_PRIV_FILE > /dev/null 2> /dev/null +AUDITOR_PUB=`gnunet-ecc -p $AUDITOR_PRIV_FILE` + +# patch configuration +TALER_DB=talercheck +taler-config -c $CONF -s exchange -o MASTER_PUBLIC_KEY -V $MASTER_PUB +taler-config -c $CONF -s merchant-exchange-default -o MASTER_KEY -V $MASTER_PUB +taler-config -c $CONF -s exchangedb-postgres -o CONFIG -V postgres:///$TALER_DB +taler-config -c $CONF -s auditordb-postgres -o CONFIG -V postgres:///$TALER_DB +taler-config -c $CONF -s merchantdb-postgres -o CONFIG -V postgres:///$TALER_DB +taler-config -c $CONF -s bank -o database -V postgres:///$TALER_DB +taler-config -c $CONF -s exchange -o KEYDIR -V "${TMP_DIR}/keydir/" +taler-config -c $CONF -s exchange -o REVOCATION_DIR -V "${TMP_DIR}/revdir/" + +echo " OK" + +echo -n "Setting up exchange ..." + +# reset database +dropdb $TALER_DB >/dev/null 2>/dev/null || true +createdb $TALER_DB || exit_skip "Could not create database $TALER_DB" +taler-exchange-dbinit -c $CONF +taler-merchant-dbinit -c $CONF +taler-auditor-dbinit -c $CONF +taler-auditor-exchange -c $CONF -m $MASTER_PUB -u $EXCHANGE_URL + +echo " OK" + +# Launch services +echo -n "Launching taler services ..." +taler-bank-manage-testing $CONF postgres:///$TALER_DB serve > taler-bank.log 2> taler-bank.err & +taler-exchange-secmod-eddsa -c $CONF 2> taler-exchange-secmod-eddsa.log & +taler-exchange-secmod-rsa -c $CONF 2> taler-exchange-secmod-rsa.log & +taler-exchange-httpd -c $CONF 2> taler-exchange-httpd.log & +taler-merchant-httpd -c $CONF -L INFO 2> taler-merchant-httpd.log & +taler-exchange-wirewatch -c $CONF 2> taler-exchange-wirewatch.log & +taler-auditor-httpd -L INFO -c $CONF 2> taler-auditor-httpd.log & + +echo " OK" + +echo -n "Launching anastasis services ..." +PREFIX="" #valgrind +$PREFIX anastasis-httpd -c $CONF_1 2> anastasis-httpd_1.log & +$PREFIX anastasis-httpd -c $CONF_2 2> anastasis-httpd_2.log & +$PREFIX anastasis-httpd -c $CONF_3 2> anastasis-httpd_3.log & +$PREFIX anastasis-httpd -c $CONF_4 2> anastasis-httpd_4.log & + +# Wait for bank to be available (usually the slowest) +for n in `seq 1 50` +do + echo -n "." + sleep 0.2 + OK=0 + # bank + wget --tries=1 --timeout=1 http://localhost:8082/ -o /dev/null -O /dev/null >/dev/null || continue + OK=1 + break +done + +if [ 1 != $OK ] +then + exit_skip "Failed to launch services (bank)" +fi + +# Wait for all other taler services to be available +for n in `seq 1 50` +do + echo -n "." + sleep 0.1 + OK=0 + # exchange + wget --tries=1 --timeout=1 http://localhost:8081/seed -o /dev/null -O /dev/null >/dev/null || continue + # merchant + wget --tries=1 --timeout=1 http://localhost:9966/ -o /dev/null -O /dev/null >/dev/null || continue + # auditor + wget --tries=1 --timeout=1 http://localhost:8083/ -o /dev/null -O /dev/null >/dev/null || continue + OK=1 + break +done + +if [ 1 != $OK ] +then + exit_skip "Failed to launch taler services" +fi + +echo "OK" + +echo -n "Setting up keys ..." +taler-exchange-offline -c $CONF \ + download \ + sign \ + enable-account payto://x-taler-bank/localhost/Exchange \ + enable-auditor $AUDITOR_PUB $AUDITOR_URL "TESTKUDOS Auditor" \ + wire-fee now x-taler-bank TESTKUDOS:0.01 TESTKUDOS:0.01 \ + upload &> taler-exchange-offline.log + +echo -n "." + +for n in `seq 1 3` +do + echo -n "." + OK=0 + wget --tries=1 --timeout=1 http://localhost:8081/keys -o /dev/null -O /dev/null >/dev/null || continue + OK=1 + break +done + +if [ 1 != $OK ] +then + exit_skip "Failed to setup keys" +fi + +echo " OK" + +echo -n "Setting up auditor signatures ..." +taler-auditor-offline -c $CONF \ + download sign upload &> taler-auditor-offline.log +echo " OK" + +echo -n "Waiting for anastasis services ..." + +# Wait for anastasis services to be available +for n in `seq 1 50` +do + echo -n "." + sleep 0.1 + OK=0 + # anastasis_01 + wget --tries=1 --timeout=1 http://localhost:8086/ -o /dev/null -O /dev/null >/dev/null || continue + # anastasis_02 + wget --tries=1 --timeout=1 http://localhost:8087/ -o /dev/null -O /dev/null >/dev/null || continue + # anastasis_03 + wget --tries=1 --timeout=1 http://localhost:8088/ -o /dev/null -O /dev/null >/dev/null || continue + # anastasis_04 + wget --tries=1 --timeout=1 http://localhost:8089/ -o /dev/null -O /dev/null >/dev/null || continue + OK=1 + break +done + +if [ 1 != $OK ] +then + exit_skip "Failed to launch anastasis services" +fi +echo "OK" + +echo -n "Configuring merchant instance ..." +# Setup merchant + +curl -H "Content-Type: application/json" -X POST -d '{"auth":{"method":"external"},"payto_uris":["payto://x-taler-bank/localhost/43"],"id":"default","name":"default","address":{},"jurisdiction":{},"default_max_wire_fee":"TESTKUDOS:1", "default_max_deposit_fee":"TESTKUDOS:1","default_wire_fee_amortization":1,"default_wire_transfer_delay":{"d_ms" : 3600000},"default_pay_delay":{"d_ms": 3600000}}' http://localhost:9966/private/instances + + +echo " DONE" + +echo -n "Running backup logic ...," +anastasis-reducer -b > $B1FILE +echo -n "." +anastasis-reducer -a \ + '{"continent": "Testcontinent"}' \ + select_continent < $B1FILE > $B2FILE +echo -n "." +anastasis-reducer -a \ + '{"country_code": "xx", + "currencies":["TESTKUDOS"]}' \ + select_country < $B2FILE > $B1FILE +echo -n "." +anastasis-reducer -a \ + '{"identity_attributes": { + "full_name": "Max Musterman", + "sq_number": "4", + "birthdate": "2000-01-01"}}' \ + enter_user_attributes < $B1FILE > $B2FILE +echo -n "," +# "91GPWWR" encodes "Hans" +anastasis-reducer -a \ + '{"authentication_method": { + "type": "question", + "instructions": "What is your name?", + "challenge": "91GPWWR" + } }' \ + add_authentication < $B2FILE > $B1FILE +echo -n "." +# "64S36" encodes "123" +anastasis-reducer -a \ + '{"authentication_method": { + "type": "question", + "instructions": "How old are you?", + "challenge": "64S36" + } }' \ + add_authentication < $B1FILE > $B2FILE +echo -n "." +# "9NGQ4WR" encodes "Mars" +anastasis-reducer -a \ + '{"authentication_method": { + "type": "question", + "instructions": "Where do you live?", + "challenge": "9NGQ4WR" + } }' \ + add_authentication < $B2FILE > $B1FILE +echo -n "." +# Finished adding authentication methods +anastasis-reducer \ + next < $B1FILE > $B2FILE + + +echo -n "," +# Finished policy review +anastasis-reducer \ + next < $B2FILE > $B1FILE +echo -n "." + +# Note: 'secret' must here be a Crockford base32-encoded value +anastasis-reducer -a \ + '{"secret": { "value" : "VERYHARDT0GVESSSECRET", "mime" : "text/plain" }}' \ + enter_secret < $B1FILE > $B2FILE +mv $B2FILE $B1FILE +anastasis-reducer next $B1FILE $B2FILE +echo " OK" + + +echo -n "Preparing wallet" +rm $WALLET_DB +taler-wallet-cli --no-throttle --wallet-db=$WALLET_DB api 'withdrawTestBalance' \ + "$(jq -n ' + { + amount: "TESTKUDOS:100", + bankBaseUrl: $BANK_URL, + exchangeBaseUrl: $EXCHANGE_URL + }' \ + --arg BANK_URL "$BANK_URL" \ + --arg EXCHANGE_URL "$EXCHANGE_URL" + )" 2> /dev/null >/dev/null +taler-wallet-cli --wallet-db=$WALLET_DB run-until-done 2>/dev/null >/dev/null +echo " OK" + +echo -en "Making payments for truth uploads ... " +OBJECT_SIZE=`jq -r -e '.payments | length' < $B2FILE` +for ((INDEX=0; INDEX < $OBJECT_SIZE; INDEX++)) +do + PAY_URI=`jq --argjson INDEX $INDEX -r -e '.payments[$INDEX]' < $B2FILE` + # run wallet CLI + echo -n "$INDEX" + taler-wallet-cli --wallet-db=$WALLET_DB handle-uri $PAY_URI -y 2>/dev/null >/dev/null + echo -n ", " +done +echo "OK" +echo -e "Running wallet run-pending..." +taler-wallet-cli --wallet-db=$WALLET_DB run-pending 2>/dev/null >/dev/null +echo -e "Payments done" + +export B2FILE +export B1FILE + +echo -en "Try to upload again ..." +$PREFIX anastasis-reducer pay $B2FILE $B1FILE +mv $B1FILE $B2FILE +echo " OK" + +echo -en "Making payments for policy uploads ... " +OBJECT_SIZE=`jq -r -e '.policy_payment_requests | length' < $B2FILE` +for ((INDEX=0; INDEX < $OBJECT_SIZE; INDEX++)) +do + PAY_URI=`jq --argjson INDEX $INDEX -r -e '.policy_payment_requests[$INDEX].payto' < $B2FILE` + # run wallet CLI + echo -n "$INDEX" + taler-wallet-cli --wallet-db=$WALLET_DB handle-uri $PAY_URI -y 2>/dev/null >/dev/null + echo -n ", " +done +echo " OK" +echo -en "Running wallet run-pending..." +taler-wallet-cli --wallet-db=$WALLET_DB run-pending 2>/dev/null >/dev/null +echo -e " payments DONE" + +echo -en "Try to upload again ..." +anastasis-reducer \ + pay < $B2FILE > $B1FILE +echo " OK: Backup finished" +echo -n "Final backup checks ..." +STATE=`jq -r -e .backup_state < $B1FILE` +if test "$STATE" != "BACKUP_FINISHED" +then + exit_fail "Expected new state to be 'BACKUP_FINISHED', got '$STATE'" +fi + +jq -r -e .core_secret < $B1FILE > /dev/null && exit_fail "'core_secret' was not cleared upon success" + +echo " OK" + +echo -n "Running recovery basic logic ..." +anastasis-reducer -r > $R1FILE +anastasis-reducer -a \ + '{"continent": "Testcontinent"}' \ + select_continent < $R1FILE > $R2FILE +anastasis-reducer -a \ + '{"country_code": "xx", + "currencies":["TESTKUDOS"]}' \ + select_country < $R2FILE > $R1FILE +anastasis-reducer -a '{"identity_attributes": { "full_name": "Max Musterman", "sq_number": "4", "birthdate": "2000-01-01" }}' enter_user_attributes < $R1FILE > $R2FILE + + +STATE=`jq -r -e .recovery_state < $R2FILE` +if test "$STATE" != "SECRET_SELECTING" +then + exit_fail "Expected new state to be 'SECRET_SELECTING', got '$STATE'" +fi +echo " OK" + +echo -n "Selecting default secret" +mv $R2FILE $R1FILE +anastasis-reducer next < $R1FILE > $R2FILE + +STATE=`jq -r -e .recovery_state < $R2FILE` +if test "$STATE" != "CHALLENGE_SELECTING" +then + exit_fail "Expected new state to be 'CHALLENGE_SELECTING', got '$STATE'" +fi +echo " OK" + +echo -n "Running challenge logic ..." + +UUID0=`jq -r -e .recovery_information.challenges[0].uuid < $R2FILE` +UUID1=`jq -r -e .recovery_information.challenges[1].uuid < $R2FILE` +UUID2=`jq -r -e .recovery_information.challenges[2].uuid < $R2FILE` +UUID0Q=`jq -r -e .recovery_information.challenges[0].instructions < $R2FILE` +UUID1Q=`jq -r -e .recovery_information.challenges[1].instructions < $R2FILE` +UUID2Q=`jq -r -e .recovery_information.challenges[2].instructions < $R2FILE` + +if test "$UUID2Q" = 'How old are you?' +then + AGE_UUID=$UUID2 +elif test "$UUID1Q" = 'How old are you?' +then + AGE_UUID=$UUID1 +else + AGE_UUID=$UUID0 +fi + +if test "$UUID2Q" = 'What is your name?' +then + NAME_UUID=$UUID2 +elif test "$UUID1Q" = 'What is your name?' +then + NAME_UUID=$UUID1 +else + NAME_UUID=$UUID0 +fi + +anastasis-reducer -a \ + "$(jq -n ' + { + uuid: $UUID + }' \ + --arg UUID "$NAME_UUID" + )" \ + select_challenge < $R2FILE > $R1FILE + +anastasis-reducer -a '{"answer": "Hans"}' \ + solve_challenge < $R1FILE > $R2FILE + +anastasis-reducer -a \ + "$(jq -n ' + { + uuid: $UUID + }' \ + --arg UUID "$AGE_UUID" + )" \ + select_challenge < $R2FILE > $R1FILE + +anastasis-reducer -a '{"answer": "123"}' \ + solve_challenge < $R1FILE > $R2FILE + +echo " OK" + +echo -n "Checking recovered secret ..." +# finally: check here that we recovered the secret... + +STATE=`jq -r -e .recovery_state < $R2FILE` +if test "$STATE" != "RECOVERY_FINISHED" +then + jq -e . $R2FILE + exit_fail "Expected new state to be 'RECOVERY_FINISHED', got '$STATE'" +fi + +SECRET=`jq -r -e .core_secret.value < $R2FILE` +if test "$SECRET" != "VERYHARDT0GVESSSECRET" +then + jq -e . $R2FILE + exit_fail "Expected recovered secret to be 'VERYHARDT0GVESSSECRET', got '$SECRET'" +fi + +MIME=`jq -r -e .core_secret.mime < $R2FILE` +if test "$MIME" != "text/plain" +then + jq -e . $R2FILE + exit_fail "Expected recovered mime to be 'text/plain', got '$MIME'" +fi + +echo " OK" + +exit 0 diff --git a/src/cli/test_anastasis_reducer_select_continent.sh b/src/cli/test_anastasis_reducer_select_continent.sh new file mode 100755 index 0000000..4cd8a84 --- /dev/null +++ b/src/cli/test_anastasis_reducer_select_continent.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " FAIL: $1" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + for n in `jobs -p` + do + kill $n 2> /dev/null || true + done + rm -f $SFILE $TFILE + wait +} + +# Install cleanup handler (except for kill -9) +SFILE=`mktemp test_reducer_stateXXXXXX` +TFILE=`mktemp test_reducer_stateXXXXXX` + +trap cleanup EXIT + +# Check we can actually run +echo -n "Testing for jq ..." +jq -h > /dev/null || exit_skip "jq required" +echo " FOUND" +echo -n "Testing for anastasis-reducer ..." +anastasis-reducer -h > /dev/null || exit_skip "anastasis-reducer required" +echo " FOUND" + +# Test continent selection in a backup state +echo -n "Test continent selection in a backup state ..." +anastasis-reducer -a '{"continent": "Testcontinent"}' select_continent resources/00-backup.json $TFILE + +STATE=`jq -r -e .backup_state < $TFILE` +if test "$STATE" != "COUNTRY_SELECTING" +then + exit_fail "Expected new state to be COUNTRY_SELECTING, got $STATE" +fi +SELECTED_CONTINENT=`jq -r -e .selected_continent < $TFILE` +if test "$SELECTED_CONTINENT" != "Testcontinent" +then + exit_fail "Expected selected continent to be Testcontinent, got $SELECTED_CONTINENT" +fi +COUNTRIES=`jq -r -e .countries < $TFILE` +if test "$COUNTRIES" == NULL +then + exit_fail "Expected country array (countries) not to be NULL" +fi +echo " OK" + + +echo -n "Test invalid continent selection ..." +anastasis-reducer -a '{"continent": "Pangaia"}' select_continent resources/00-recovery.json $TFILE 2> /dev/null \ + && exit_fail "Expected selection to fail. Check '$TFILE'" + +echo " OK" + +echo -n "Test continent selection in a recovery state ..." +anastasis-reducer -a '{"continent": "Testcontinent"}' select_continent resources/00-recovery.json $TFILE + +STATE=`jq -r -e .recovery_state < $TFILE` +if test "$STATE" != "COUNTRY_SELECTING" +then + exit_fail "Expected new state to be COUNTRY_SELECTING, got $STATE" +fi +jq -e .countries[0] < $TFILE > /dev/null || exit_fail "Expected new state to include countries" +jq -e .countries[0].code < $TFILE > /dev/null || exit_fail "Expected new state to include countries with code" +jq -e .countries[0].continent < $TFILE > /dev/null || exit_fail "Expected new state to include countries with continent" +jq -e .countries[0].name < $TFILE > /dev/null || exit_fail "Expected new state to include countries with name" +jq -e .countries[0].currency < $TFILE > /dev/null || exit_fail "Expected new state to include countries with currency" + +SELECTED_CONTINENT=`jq -r -e .selected_continent < $TFILE` +if test "$SELECTED_CONTINENT" != "Testcontinent" +then + exit_fail "Expected selected continent to be 'Testcontinent', got $SELECTED_CONTINENT" +fi + +COUNTRIES=`jq -r -e .countries < $TFILE` +if test "$COUNTRIES" == NULL +then + exit_fail "Expected country array (countries) not to be NULL" +fi +jq -e .countries[0] < $TFILE > /dev/null || exit_fail "Expected new state to include countries" +jq -e .countries[0].code < $TFILE > /dev/null || exit_fail "Expected new state to include countries with code" +jq -e .countries[0].continent < $TFILE > /dev/null || exit_fail "Expected new state to include countries with continent" +jq -e .countries[0].name < $TFILE > /dev/null || exit_fail "Expected new state to include countries with name" +jq -e .countries[0].currency < $TFILE > /dev/null || exit_fail "Expected new state to include countries with currency" + +echo " OK" + + +# Test missing arguments in a recovery state +echo -n "Test bogus country selection in a recovery state ..." +anastasis-reducer -a '{"country": "Germany"}' select_continent resources/00-recovery.json $TFILE 2> /dev/null && exit_fail "Expected state transition to fail, but it worked, check $TFILE" + +echo " OK" + +# Test continent selection in a recovery state +echo -n "Test bogus continent selection in a recovery state ..." +anastasis-reducer -a '{"continent": "Germany"}' select_continent resources/00-recovery.json $TFILE 2> /dev/null && exit_fail "Expected state transition to fail, but it worked, check $TFILE" + +echo " OK" + +exit 0 diff --git a/src/cli/test_anastasis_reducer_select_country.sh b/src/cli/test_anastasis_reducer_select_country.sh new file mode 100755 index 0000000..db17052 --- /dev/null +++ b/src/cli/test_anastasis_reducer_select_country.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " FAIL: $1" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + for n in `jobs -p` + do + kill $n 2> /dev/null || true + done + rm -f $TFILE + wait +} + + + +TFILE=`mktemp test_reducer_stateXXXXXX` + +# Install cleanup handler (except for kill -9) +trap cleanup EXIT + +# Check we can actually run +echo -n "Testing for jq" +jq -h > /dev/null || exit_skip "jq required" +echo " FOUND" + +echo -n "Testing for anastasis-reducer ..." +anastasis-reducer -h > /dev/null || exit_skip "anastasis-reducer required" +echo " FOUND" + + + +# Test continent re-selection +echo -n "Test continent re-selection ..." +anastasis-reducer -a '{"continent": "Europe"}' select_continent resources/01-recovery.json $TFILE + +echo -n "." + + +STATE=`jq -r -e .recovery_state < $TFILE` +if test "$STATE" != "COUNTRY_SELECTING" +then + exit_fail "Expected new state to be COUNTRY_SELECTING, got $STATE" +fi + +echo -n "." + +jq -e .countries[0] < $TFILE > /dev/null || exit_fail "Expected new state to include countries" +jq -e .countries[0].code < $TFILE > /dev/null || exit_fail "Expected new state to include countries with code" +jq -e .countries[0].continent < $TFILE > /dev/null || exit_fail "Expected new state to include countries with continent" +jq -e .countries[0].name < $TFILE > /dev/null || exit_fail "Expected new state to include countries with name" +jq -e .countries[0].currency < $TFILE > /dev/null || exit_fail "Expected new state to include countries with currency" + +SELECTED_CONTINENT=`jq -r -e .selected_continent < $TFILE` +if test "$SELECTED_CONTINENT" != "Europe" +then + exit_fail "Expected selected continent to be 'Testcontinent', got $SELECTED_CONTINENT" +fi + +echo " OK" + + +echo -n "Test invalid continent re-selection ..." +anastasis-reducer -a '{"continent": "Pangaia"}' select_continent resources/00-recovery.json $TFILE 2> /dev/null \ + && exit_fail "Expected selection to fail. Check '$TFILE'" + +echo " OK" + + +echo -n "Test NX country selection ..." + +anastasis-reducer -a \ + '{"country_code": "zz", + "currencies": ["EUR" ]}' \ + select_country \ + resources/01-backup.json $TFILE 2> /dev/null \ + && exit_fail "Expected selection to fail. Check '$TFILE'" + +echo " OK" + +echo -n "Test invalid country selection for continent ..." + +anastasis-reducer -a \ + '{"country_code": "de", + "currencies":["EUR"]}' \ + select_country \ + resources/01-backup.json $TFILE 2> /dev/null \ + && exit_fail "Expected selection to fail. Check '$TFILE'" + +echo " OK" + +echo -n "Test country selection ..." + +anastasis-reducer -a \ + '{"country_code": "xx", + "currencies":["TESTKUDOS"]}' \ + select_country resources/01-backup.json $TFILE + +STATE=`jq -r -e .backup_state < $TFILE` +if test "$STATE" != "USER_ATTRIBUTES_COLLECTING" +then + exit_fail "Expected new state to be 'USER_ATTRIBUTES_COLLECTING', got '$STATE'" +fi +echo -n "." +SELECTED_COUNTRY=`jq -r -e .selected_country < $TFILE` +if test "$SELECTED_COUNTRY" != "xx" +then + exit_fail "Expected selected country to be 'xx', got '$SELECTED_COUNTRY'" +fi +echo -n "." +SELECTED_CURRENCY=`jq -r -e .currencies[0] < $TFILE` +if test "$SELECTED_CURRENCY" != "TESTKUDOS" +then + exit_fail "Expected selected currency to be 'TESTKUDOS', got '$SELECTED_CURRENCY'" +fi +echo -n "." +REQ_ATTRIBUTES=`jq -r -e .required_attributes < $TFILE` +if test "$REQ_ATTRIBUTES" == NULL +then + exit_fail "Expected required attributes array not to be NULL" +fi +echo -n "." +AUTH_PROVIDERS=`jq -r -e .authentication_providers < $TFILE` +if test "$AUTH_PROVIDERS" == NULL +then + exit_fail "Expected authentication_providers array not to be NULL" +fi + +echo " OK" + +exit 0 diff --git a/src/cli/test_reducer.conf b/src/cli/test_reducer.conf new file mode 100644 index 0000000..a4baaed --- /dev/null +++ b/src/cli/test_reducer.conf @@ -0,0 +1,197 @@ +[PATHS] +TALER_HOME = ${PWD}/test_reducer_home/ +TALER_DATA_HOME = $TALER_HOME/.local/share/taler/ +TALER_CONFIG_HOME = $TALER_HOME/.config/taler/ +TALER_CACHE_HOME = $TALER_HOME/.cache/taler/ +TALER_RUNTIME_DIR = ${TMPDIR:-${TMP:-/tmp}}/${USER:-}/taler-system-runtime/ + +[taler] +CURRENCY = TESTKUDOS +CURRENCY_ROUND_UNIT = TESTKUDOS:0.01 + +[anastasis] +DB = postgres +PAYMENT_BACKEND_URL = http://localhost:9966/ +ANNUAL_FEE = TESTKUDOS:4.99 +TRUTH_UPLOAD_FEE = TESTKUDOS:0.01 +UPLOAD_LIMIT_MB = 1 +ANNUAL_POLICY_UPLOAD_LIMIT = 128 +INSURANCE = TESTKUDOS:1.0 + +[authorization-question] +COST = TESTKUDOS:0.0 + + +[exchange] +MAX_KEYS_CACHING = forever +DB = postgres +MASTER_PRIV_FILE = ${TALER_DATA_HOME}/exchange/offline-keys/master.priv +SERVE = tcp +UNIXPATH = ${TALER_RUNTIME_DIR}/exchange.http +UNIXPATH_MODE = 660 +PORT = 8081 +BASE_URL = http://localhost:8081/ +SIGNKEY_DURATION = 2 weeks +SIGNKEY_LEGAL_DURATION = 2 years +LEGAL_DURATION = 2 years +LOOKAHEAD_SIGN = 3 weeks 1 day +LOOKAHEAD_PROVIDE = 2 weeks 1 day +KEYDIR = ${TALER_DATA_HOME}/exchange/live-keys/ +REVOCATION_DIR = ${TALER_DATA_HOME}/exchange/revocations/ +TERMS_ETAG = 0 +PRIVACY_ETAG = 0 + +[merchant] +SERVE = tcp +PORT = 9966 +UNIXPATH = ${TALER_RUNTIME_DIR}/merchant.http +UNIXPATH_MODE = 660 +DEFAULT_WIRE_FEE_AMORTIZATION = 1 +DB = postgres +WIREFORMAT = default +# Set very low, so we can be sure that the database generated +# will contain wire transfers "ready" for the aggregator. +WIRE_TRANSFER_DELAY = 1 minute +DEFAULT_PAY_DEADLINE = 1 day +DEFAULT_MAX_DEPOSIT_FEE = TESTKUDOS:0.1 +KEYFILE = ${TALER_DATA_HOME}/merchant/merchant.priv +DEFAULT_MAX_WIRE_FEE = TESTKUDOS:0.10 + +# Ensure that merchant reports EVERY deposit confirmation to auditor +FORCE_AUDIT = YES + +[auditor] +DB = postgres +AUDITOR_PRIV_FILE = ${TALER_DATA_HOME}/auditor/offline-keys/auditor.priv +SERVE = tcp +UNIXPATH = ${TALER_RUNTIME_DIR}/exchange.http +UNIXPATH_MODE = 660 +PORT = 8083 +AUDITOR_URL = http://localhost:8083/ +TINY_AMOUNT = TESTKUDOS:0.01 +AUDITOR_PRIV_FILE = ${TALER_DATA_HOME}/auditor/offline-keys/auditor.priv +BASE_URL = "http://localhost:8083/" + +[bank] +DATABASE = postgres:///taler-auditor-basedb +MAX_DEBT = TESTKUDOS:50.0 +MAX_DEBT_BANK = TESTKUDOS:100000.0 +HTTP_PORT = 8082 +SUGGESTED_EXCHANGE = http://localhost:8081/ +SUGGESTED_EXCHANGE_PAYTO = payto://x-taler-bank/localhost/2 +ALLOW_REGISTRATIONS = YES +SERVE = http + +[exchangedb] +IDLE_RESERVE_EXPIRATION_TIME = 4 weeks +LEGAL_RESERVE_EXPIRATION_TIME = 7 years + +[exchange-account-1] +PAYTO_URI = payto://x-taler-bank/localhost/Exchange +enable_debit = yes +enable_credit = yes +WIRE_GATEWAY_URL = "http://localhost:8082/taler-wire-gateway/Exchange/" +WIRE_GATEWAY_AUTH_METHOD = basic +USERNAME = Exchange +PASSWORD = x + +[merchant-exchange-default] +EXCHANGE_BASE_URL = http://localhost:8081/ +CURRENCY = TESTKUDOS + +[payments-generator] +currency = TESTKUDOS +instance = default +bank = http://localhost:8082/ +merchant = http://localhost:9966/ +exchange_admin = http://localhost:18080/ +exchange-admin = http://localhost:18080/ +exchange = http://localhost:8081/ + +[coin_kudos_ct_1] +value = TESTKUDOS:0.01 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = TESTKUDOS:0.01 +fee_deposit = TESTKUDOS:0.01 +fee_refresh = TESTKUDOS:0.01 +fee_refund = TESTKUDOS:0.01 +rsa_keysize = 1024 + +[coin_kudos_ct_10] +value = TESTKUDOS:0.10 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = TESTKUDOS:0.01 +fee_deposit = TESTKUDOS:0.01 +fee_refresh = TESTKUDOS:0.03 +fee_refund = TESTKUDOS:0.01 +rsa_keysize = 1024 + +[coin_kudos_1] +value = TESTKUDOS:1 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = TESTKUDOS:0.02 +fee_deposit = TESTKUDOS:0.02 +fee_refresh = TESTKUDOS:0.03 +fee_refund = TESTKUDOS:0.01 +rsa_keysize = 1024 + +[coin_kudos_2] +value = TESTKUDOS:2 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = TESTKUDOS:0.03 +fee_deposit = TESTKUDOS:0.03 +fee_refresh = TESTKUDOS:0.04 +fee_refund = TESTKUDOS:0.02 +rsa_keysize = 1024 + +[coin_kudos_4] +value = TESTKUDOS:4 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = TESTKUDOS:0.03 +fee_deposit = TESTKUDOS:0.03 +fee_refresh = TESTKUDOS:0.04 +fee_refund = TESTKUDOS:0.02 +rsa_keysize = 1024 + +[coin_kudos_5] +value = TESTKUDOS:5 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = TESTKUDOS:0.01 +fee_deposit = TESTKUDOS:0.01 +fee_refresh = TESTKUDOS:0.03 +fee_refund = TESTKUDOS:0.01 +rsa_keysize = 1024 + +[coin_kudos_8] +value = TESTKUDOS:8 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = TESTKUDOS:0.05 +fee_deposit = TESTKUDOS:0.02 +fee_refresh = TESTKUDOS:0.03 +fee_refund = TESTKUDOS:0.04 +rsa_keysize = 1024 + +[coin_kudos_10] +value = TESTKUDOS:10 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = TESTKUDOS:0.01 +fee_deposit = TESTKUDOS:0.01 +fee_refresh = TESTKUDOS:0.03 +fee_refund = TESTKUDOS:0.01 +rsa_keysize = 1024 diff --git a/src/cli/user-details-example.json b/src/cli/user-details-example.json new file mode 100644 index 0000000..3eb1f7a --- /dev/null +++ b/src/cli/user-details-example.json @@ -0,0 +1,6 @@ +{ + "first-name":"John", + "last-name":"Wayne", + "birthdate":"01-01-1901", + "social-security-number":"123456789" +}
\ No newline at end of file diff --git a/src/include/Makefile.am b/src/include/Makefile.am new file mode 100644 index 0000000..f9f6548 --- /dev/null +++ b/src/include/Makefile.am @@ -0,0 +1,15 @@ +# This Makefile.am is in the public domain +anastasisincludedir = $(includedir)/anastasis + +anastasisinclude_HEADERS = \ + platform.h gettext.h \ + anastasis_database_plugin.h \ + anastasis_service.h \ + anastasis_error_codes.h \ + anastasis_database_lib.h \ + anastasis_util_lib.h \ + anastasis_crypto_lib.h \ + anastasis_redux.h \ + anastasis_authorization_plugin.h \ + anastasis_authorization_lib.h \ + anastasis.h diff --git a/src/include/anastasis.h b/src/include/anastasis.h new file mode 100644 index 0000000..1591106 --- /dev/null +++ b/src/include/anastasis.h @@ -0,0 +1,986 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis.h + * @brief anastasis high-level client api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#ifndef ANASTASIS_H +#define ANASTASIS_H + +#include "anastasis_service.h" +#include <taler/taler_json_lib.h> +#include <gnunet/gnunet_util_lib.h> +#include <stdbool.h> + + +/* ********************* Recovery api *********************** */ + +/** + * Defines the instructions for a challenge, what does the user have + * to do to fulfill the challenge. Also defines the method and other + * information for the challenge like a link for the video indent or a + * information to which address an e-mail was sent. + */ +struct ANASTASIS_Challenge; + + +/** + * Defines the instructions for a challenge, what does the user have + * to do to fulfill the challenge. Also defines the method and other + * information for the challenge like a link for the video indent or a + * information to which address an e-mail was sent. + */ +struct ANASTASIS_ChallengeDetails +{ + + /** + * UUID which identifies this challenge + */ + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + + /** + * Which type is this challenge (E-Mail, Security Question, SMS...) + */ + const char *type; + + /** + * Defines the base URL of the Anastasis provider used for the challenge. + */ + const char *provider_url; + + /** + * Instructions for solving the challenge (generic, set client-side + * when challenge was established). + */ + const char *instructions; + + /** + * true if challenged was already solved, else false. + */ + bool solved; + +}; + + +/** + * Return public details about a challenge. + * + * @param challenge the challenge to inspect + * @return public details about the challenge + */ +const struct ANASTASIS_ChallengeDetails * +ANASTASIS_challenge_get_details (struct ANASTASIS_Challenge *challenge); + + +/** + * Possible outcomes of trying to start a challenge operation. + */ +enum ANASTASIS_ChallengeStatus +{ + + /** + * The challenge has been solved. + */ + ANASTASIS_CHALLENGE_STATUS_SOLVED, + + /** + * Instructions for how to solve the challenge are provided. Also + * used if the answer we provided was wrong (or if no answer was + * provided, but one is needed). + */ + ANASTASIS_CHALLENGE_STATUS_INSTRUCTIONS, + + /** + * A redirection URL needed to solve the challenge is provided. Also + * used if the answer we provided was wrong (or if no answer was + * provided, but one is needed). + */ + ANASTASIS_CHALLENGE_STATUS_REDIRECT_FOR_AUTHENTICATION, + + /** + * Payment is required before the challenge can be answered. + */ + ANASTASIS_CHALLENGE_STATUS_PAYMENT_REQUIRED, + + /** + * We encountered an error talking to the Anastasis service. + */ + ANASTASIS_CHALLENGE_STATUS_SERVER_FAILURE, + + /** + * The server does not know this truth. + */ + ANASTASIS_CHALLENGE_STATUS_TRUTH_UNKNOWN, + + /** + * The rate limit for solving the challenge was exceeded. + */ + ANASTASIS_CHALLENGE_STATUS_RATE_LIMIT_EXCEEDED + +}; + + +/** + * Response from an #ANASTASIS_challenge_start() operation. + */ +struct ANASTASIS_ChallengeStartResponse +{ + /** + * What is our status on satisfying this challenge. Determines @e details. + */ + enum ANASTASIS_ChallengeStatus cs; + + /** + * Which challenge is this about? + */ + struct ANASTASIS_Challenge *challenge; + + /** + * Details depending on @e cs + */ + union + { + + /** + * Challenge details provided if + * @e cs is #ANASTASIS_CHALLENGE_STATUS_INSTRUCTIONS. + */ + struct + { + + /** + * Response with server-side instructions for the user. + */ + const void *body; + + /** + * Mime type of the data in @e body. + */ + const char *content_type; + + /** + * Number of bytes in @e body + */ + size_t body_size; + + /** + * HTTP status returned by the server. #MHD_HTTP_ALREADY_REPORTED + * if the server did already send the challenge to the user, + * #MHD_HTTP_FORBIDDEN if the answer was wrong (or missing). + */ + unsigned int http_status; + } open_challenge; + + + /** + * Response with URL to redirect the user to, if + * @e cs is #ANASTASIS_CHALLENGE_STATUS_REDIRECT_FOR_AUTHENTICATION. + */ + const char *redirect_url; + + /** + * Response with instructions for how to pay, if + * @e cs is #ANASTASIS_CHALLENGE_STATUS_PAYMENT_REQUIRED. + */ + struct + { + + /** + * "taler://pay" URI with details how to pay for the challenge. + */ + const char *taler_pay_uri; + + /** + * Payment secret from @e taler_pay_uri. + */ + struct ANASTASIS_PaymentSecretP payment_secret; + + } payment_required; + + + /** + * Response with details about a server-side failure, if + * @e cs is #ANASTASIS_CHALLENGE_STATUS_SERVER_FAILURE. + */ + struct + { + + /** + * HTTP status returned by the server. + */ + unsigned int http_status; + + /** + * Taler-specific error code. + */ + enum TALER_ErrorCode ec; + + } server_failure; + + } details; +}; + + +/** + * Defines a callback for the response status for a challenge start + * operation. + * + * @param cls closure + * @param csr response details + */ +typedef void +(*ANASTASIS_AnswerFeedback)( + void *cls, + const struct ANASTASIS_ChallengeStartResponse *csr); + + +/** + * User starts a challenge which reponds out of bounds (E-Mail, SMS, + * Postal..) If the challenge is zero cost, the challenge + * instructions will be sent to the client. If the challenge needs + * payment a payment link is sent to the client. After payment the + * challenge start method has to be called again. + * + * @param c reference to the escrow challenge which is started + * @param psp payment secret, NULL if no payment was yet made + * @param timeout how long to wait for payment + * @param hashed_answer answer to the challenge, NULL if we have none yet + * @param af reference to the answerfeedback which is passed back to the user + * @param af_cls closure for @a af + * @return #GNUNET_OK if the challenge was successfully started + */ +int +ANASTASIS_challenge_start (struct ANASTASIS_Challenge *c, + const struct ANASTASIS_PaymentSecretP *psp, + struct GNUNET_TIME_Relative timeout, + const struct GNUNET_HashCode *hashed_answer, + ANASTASIS_AnswerFeedback af, + void *af_cls); + + +/** + * Challenge answer for a security question. Is referenced to + * a challenge and sends back an AnswerFeedback. Convenience + * wrapper around #ANASTASIS_challenge_start that hashes @a answer + * for security questions. + * + * @param c reference to the challenge which is answered + * @param psp information about payment made for the recovery + * @param timeout how long to wait for payment + * @param answer user input instruction defines which input is needed + * @param af reference to the answerfeedback which is passed back to the user + * @param af_cls closure for @a af + * @return #GNUNET_OK on success + */ +int +ANASTASIS_challenge_answer (struct ANASTASIS_Challenge *c, + const struct ANASTASIS_PaymentSecretP *psp, + struct GNUNET_TIME_Relative timeout, + const char *answer, + ANASTASIS_AnswerFeedback af, + void *af_cls); + + +/** + * Challenge answer from the user like input SMS TAN or e-mail wpin. Is + * referenced to a challenge and sends back an AnswerFeedback. + * Convenience wrapper around #ANASTASIS_challenge_start that hashes + * numeric (unsalted) @a answer. Variant for numeric answers. + * + * @param c reference to the challenge which is answered + * @param psp information about payment made for the recovery + * @param timeout how long to wait for payment + * @param answer user input instruction defines which input is needed + * @param af reference to the answerfeedback which is passed back to the user + * @param af_cls closure for @a af + * @return #GNUNET_OK on success + */ +int +ANASTASIS_challenge_answer2 (struct ANASTASIS_Challenge *c, + const struct ANASTASIS_PaymentSecretP *psp, + struct GNUNET_TIME_Relative timeout, + uint64_t answer, + ANASTASIS_AnswerFeedback af, + void *af_cls); + + +/** + * Abort answering challenge. + * + * @param c reference to the escrow challenge which was started + */ +void +ANASTASIS_challenge_abort (struct ANASTASIS_Challenge *c); + + +/** + * Defines a Decryption Policy with multiple escrow methods + */ +struct ANASTASIS_DecryptionPolicy +{ + /** + * Array of challenges needed to solve for this decryption policy. + */ + struct ANASTASIS_Challenge **challenges; + + /** + * Length of the @a challenges in this policy. + */ + unsigned int challenges_length; + +}; + + +/** + * Defines the recovery information (possible policies and version of the recovery document) + */ +struct ANASTASIS_RecoveryInformation +{ + + /** + * Array of @e dps_len policies that would allow recovery of the core secret. + */ + struct ANASTASIS_DecryptionPolicy **dps; + + /** + * Array of all @e cs_len challenges to be solved (for any of the policies). + */ + struct ANASTASIS_Challenge **cs; + + /** + * Name of the secret being recovered, possibly NULL. + */ + const char *secret_name; + + /** + * Length of the @e dps array. + */ + unsigned int dps_len; + + /** + * Length of the @e cs array. + */ + unsigned int cs_len; + + /** + * Actual recovery document version obtained. + */ + unsigned int version; +}; + + +/** + * Callback which passes back the recovery document and its possible + * policies. Also passes back the version of the document for the user + * to check. + * + * @param cls closure for the callback + * @param ri recovery information struct which contains the policies + */ +typedef void +(*ANASTASIS_PolicyCallback)(void *cls, + const struct ANASTASIS_RecoveryInformation *ri); + + +/** + * Possible outcomes of a recovery process. + */ +enum ANASTASIS_RecoveryStatus +{ + + /** + * Recovery succeeded. + */ + ANASTASIS_RS_SUCCESS = 0, + + /** + * The HTTP download of the policy failed. + */ + ANASTASIS_RS_POLICY_DOWNLOAD_FAILED, + + /** + * We did not get a valid policy document. + */ + ANASTASIS_RS_POLICY_DOWNLOAD_NO_POLICY, + + /** + * The decompressed policy document was too big for available memory. + */ + ANASTASIS_RS_POLICY_DOWNLOAD_TOO_BIG, + + /** + * The decrypted policy document was not compressed. + */ + ANASTASIS_RS_POLICY_DOWNLOAD_INVALID_COMPRESSION, + + /** + * The decompressed policy document was not in JSON. + */ + ANASTASIS_RS_POLICY_DOWNLOAD_NO_JSON, + + /** + * The decompressed policy document was in malformed JSON. + */ + ANASTASIS_RS_POLICY_MALFORMED_JSON, + + /** + * The Anastasis server reported a transient error. + */ + ANASTASIS_RS_POLICY_SERVER_ERROR, + + /** + * The Anastasis server no longer has a policy (likely expired). + */ + ANASTASIS_RS_POLICY_GONE, + + /** + * The Anastasis server reported that the account is unknown. + */ + ANASTASIS_RS_POLICY_UNKNOWN +}; + + +/** + * This function is called whenever the recovery process ends. + * On success, the secret is returned in @a secret. + * + * @param cls closure + * @param ec error code + * @param secret contains the core secret which is passed to the user + * @param secret_size defines the size of the core secret + */ +typedef void +(*ANASTASIS_CoreSecretCallback)(void *cls, + enum ANASTASIS_RecoveryStatus rc, + const void *secret, + size_t secret_size); + + +/** + * stores provider URIs, identity key material, decrypted recovery document (internally!) + */ +struct ANASTASIS_Recovery; + + +/** + * Starts the recovery process by opening callbacks for the coresecret and a policy callback. A list of + * providers is checked for policies and passed back to the client. + * + * @param ctx context for making HTTP requests + * @param id_data contains the users identity, (user account on providers) + * @param version defines the version which will be downloaded NULL for latest version + * @param anastasis_provider_url NULL terminated list of possible provider urls + * @param provider_salt the server salt + * @param pc opens the policy call back which holds the downloaded version and the policies + * @param pc_cls closure for callback + * @param csc core secret callback is opened, with this the core secert is passed to the client after the authentication + * @param csc_cls handle for the callback + * @return recovery operation handle + */ +struct ANASTASIS_Recovery * +ANASTASIS_recovery_begin ( + struct GNUNET_CURL_Context *ctx, + const json_t *id_data, + unsigned int version, + const char *anastasis_provider_url, + const struct ANASTASIS_CRYPTO_ProviderSaltP *provider_salt, + ANASTASIS_PolicyCallback pc, + void *pc_cls, + ANASTASIS_CoreSecretCallback csc, + void *csc_cls); + + +/** + * Serialize recovery operation state and returning it. + * The recovery MAY still continue, applications should call + * #ANASTASIS_recovery_abort() to truly end the recovery. + * + * @param r recovery operation to suspend. + * @return JSON serialized state of @a r + */ +json_t * +ANASTASIS_recovery_serialize (const struct ANASTASIS_Recovery *r); + + +/** + * Deserialize recovery operation. + * + * @param ctx context for making HTTP requests + * @param input result from #ANASTASIS_recovery_serialize() + * @param pc opens the policy call back which holds the downloaded version and the policies + * @param pc_cls closure for callback + * @param csc core secret callback is opened, with this the core secert is passed to the client after the authentication + * @param csc_cls handle for the callback + * @return recovery operation handle + */ +struct ANASTASIS_Recovery * +ANASTASIS_recovery_deserialize (struct GNUNET_CURL_Context *ctx, + const json_t *input, + ANASTASIS_PolicyCallback pc, + void *pc_cls, + ANASTASIS_CoreSecretCallback csc, + void *csc_cls); + + +/** + * Cancels the recovery process + * + * @param r handle to the recovery struct + */ +void +ANASTASIS_recovery_abort (struct ANASTASIS_Recovery *r); + + +/* ************************* Backup API ***************************** */ + + +/** + * Represents a truth object, which is a key share and the respective + * challenge to be solved with an Anastasis provider to recover the + * key share. + */ +struct ANASTASIS_Truth; + + +/** + * Extracts truth data from JSON. + * + * @param json JSON encoding to decode; truth returned ONLY valid as long + * as the JSON remains valid (do not decref until the truth + * is truly finished) + * @return decoded truth object, NULL on error + */ +struct ANASTASIS_Truth * +ANASTASIS_truth_from_json (const json_t *json); + + +/** + * Returns JSON-encoded truth data. + * Creates a policy with a set of truth's. Creates the policy key + * with the different key shares from the @a truths. The policy key + * will then be used to encrypt/decrypt the escrow master key. + * + * @param t object to return JSON encoding for + * @return JSON encoding of @a t + */ +json_t * +ANASTASIS_truth_to_json (const struct ANASTASIS_Truth *t); + + +/** + * Handle for the operation to establish a truth object by sharing + * an encrypted key share with an Anastasis provider. + */ +struct ANASTASIS_TruthUpload; + + +/** + * Upload result information. The resulting truth object can be used + * to create policies. If payment is required, the @a taler_pay_url + * is returned and the operation must be retried after payment. + * Callee MUST free @a t using ANASTASIS_truth_free(). + * + * @param cls closure for callback + * @param t truth object to create policies, NULL on failure + * @param ud upload details, useful to continue in case of errors, NULL on success + */ +typedef void +(*ANASTASIS_TruthCallback)(void *cls, + struct ANASTASIS_Truth *t, + const struct ANASTASIS_UploadDetails *ud); + + +/** + * Uploads truth data to an escrow provider. The resulting truth object + * is returned via the @a tc function. If payment is required, it is + * requested via the @a tcp callback. + * + * @param ctx the CURL context used to connect to the backend + * @param user_id user identifier derived from user data and backend salt + * @param type defines the type of the challenge (secure question, sms, email) + * @param instructions depending on @a type! usually only for security question/answer! + * @param mime_type format of the challenge + * @param provider_salt the providers salt + * @param truth_data contains the truth for this challenge i.e. phone number, email address + * @param truth_data_size size of the @a truth_data + * @param payment_years_requested for how many years would the client like the service to store the truth? + * @param pay_timeout how long to wait for payment + * @param tc opens the truth callback which contains the status of the upload + * @param tc_cls closure for the @a tc callback + */ +struct ANASTASIS_TruthUpload * +ANASTASIS_truth_upload ( + struct GNUNET_CURL_Context *ctx, + const struct ANASTASIS_CRYPTO_UserIdentifierP *user_id, + const char *provider_url, + const char *type, + const char *instructions, + const char *mime_type, + const struct ANASTASIS_CRYPTO_ProviderSaltP *provider_salt, + const void *truth_data, + size_t truth_data_size, + uint32_t payment_years_requested, + struct GNUNET_TIME_Relative pay_timeout, + ANASTASIS_TruthCallback tc, + void *tc_cls); + + +/** + * Retries upload of truth data to an escrow provider. The resulting + * truth object is returned via the @a tc function. If payment is + * required, it is requested via the @a tcp callback. + * + * @param ctx the CURL context used to connect to the backend + * @param user_id user identifier derived from user data and backend salt + * @param type defines the type of the challenge (secure question, sms, email) + * @param instructions depending on @a type! usually only for security question/answer! + * @param mime_type format of the challenge + * @param provider_salt the providers salt + * @param truth_data contains the truth for this challenge i.e. phone number, email address + * @param truth_data_size size of the @a truth_data + * @param payment_years_requested for how many years would the client like the service to store the truth? + * @param pay_timeout how long to wait for payment + * @param nonce nonce to use for symmetric encryption + * @param uuid truth UUID to use + * @param salt salt to use to hash security questions + * @param truth_key symmetric encryption key to use to encrypt @a truth_data + * @param key_share share of the overall key to store in this truth object + * @param tc opens the truth callback which contains the status of the upload + * @param tc_cls closure for the @a tc callback + */ +struct ANASTASIS_TruthUpload * +ANASTASIS_truth_upload2 ( + struct GNUNET_CURL_Context *ctx, + const struct ANASTASIS_CRYPTO_UserIdentifierP *user_id, + const char *provider_url, + const char *type, + const char *instructions, + const char *mime_type, + const struct ANASTASIS_CRYPTO_ProviderSaltP *provider_salt, + const void *truth_data, + size_t truth_data_size, + uint32_t payment_years_requested, + struct GNUNET_TIME_Relative pay_timeout, + const struct ANASTASIS_CRYPTO_NonceP *nonce, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid, + const struct ANASTASIS_CRYPTO_QuestionSaltP *salt, + const struct ANASTASIS_CRYPTO_TruthKeyP *truth_key, + const struct ANASTASIS_CRYPTO_KeyShareP *key_share, + ANASTASIS_TruthCallback tc, + void *tc_cls); + + +/** + * Retries upload of truth data to an escrow provider using an + * existing truth object. If payment is required, it is requested via + * the @a tc callback. + * + * @param ctx the CURL context used to connect to the backend + * @param user_id user identifier derived from user data and backend salt + * @param[in] t truth details, reference is consumed + * @param truth_data contains the truth for this challenge i.e. phone number, email address + * @param truth_data_size size of the @a truth_data + * @param payment_years_requested for how many years would the client like the service to store the truth? + * @param pay_timeout how long to wait for payment + * @param tc opens the truth callback which contains the status of the upload + * @param tc_cls closure for the @a tc callback + */ +struct ANASTASIS_TruthUpload * +ANASTASIS_truth_upload3 (struct GNUNET_CURL_Context *ctx, + const struct ANASTASIS_CRYPTO_UserIdentifierP *user_id, + struct ANASTASIS_Truth *t, + const void *truth_data, + size_t truth_data_size, + uint32_t payment_years_requested, + struct GNUNET_TIME_Relative pay_timeout, + ANASTASIS_TruthCallback tc, + void *tc_cls); + + +/** + * Cancels a truth upload process. + * + * @param tu handle for the upload + */ +void +ANASTASIS_truth_upload_cancel (struct ANASTASIS_TruthUpload *tu); + + +/** + * Free's the truth object which was returned to a #ANASTASIS_TruthCallback. + * + * @param t object to clean up + */ +void +ANASTASIS_truth_free (struct ANASTASIS_Truth *t); + + +/** + * Policy object, representing a set of truths (and thus challenges + * to satisfy) to recover a secret. + */ +struct ANASTASIS_Policy; + + +/** + * Creates a policy with a set of truth's. Creates the policy key + * with the different key shares from the @a truths. The policy key + * will then be used to encrypt/decrypt the escrow master key. + * + * @param truths array of truths which are stored on different providers + * @param truths_len length of the @a truths array + */ +struct ANASTASIS_Policy * +ANASTASIS_policy_create (const struct ANASTASIS_Truth *truths[], + unsigned int truths_len); + + +/** + * Destroys a policy object. + * + * @param p handle for the policy to destroy + */ +void +ANASTASIS_policy_destroy (struct ANASTASIS_Policy *p); + + +/** + * Information about a provider requesting payment for storing a policy. + */ +struct ANASTASIS_SharePaymentRequest +{ + /** + * Payment request URL. + */ + const char *payment_request_url; + + /** + * Base URL of the provider requesting payment. + */ + const char *provider_url; + + /** + * The payment secret (aka order ID) extracted from the @e payment_request_url. + */ + struct ANASTASIS_PaymentSecretP payment_secret; +}; + + +/** + * Result of uploading share data. + */ +enum ANASTASIS_ShareStatus +{ + /** + * Upload successful. + */ + ANASTASIS_SHARE_STATUS_SUCCESS = 0, + + /** + * Upload requires payment. + */ + ANASTASIS_SHARE_STATUS_PAYMENT_REQUIRED, + + /** + * Failure to upload secret share at the provider. + */ + ANASTASIS_SHARE_STATUS_PROVIDER_FAILED +}; + + +/** + * Per-provider status upon successful backup. + */ +struct ANASTASIS_ProviderSuccessStatus +{ + /** + * Base URL of the provider. + */ + const char *provider_url; + + /** + * When will the policy expire? + */ + struct GNUNET_TIME_Absolute policy_expiration; + + /** + * Version number of the policy at the provider. + */ + unsigned long long policy_version; + +}; + + +/** + * Complete result of a secret sharing operation. + */ +struct ANASTASIS_ShareResult +{ + /** + * Status of the share secret operation. + */ + enum ANASTASIS_ShareStatus ss; + + /** + * Details about the result, depending on @e ss. + */ + union + { + + struct + { + + /** + * Array of status details for each provider. + */ + const struct ANASTASIS_ProviderSuccessStatus *pss; + + /** + * Length of the @e policy_version and @e provider_urls arrays. + */ + unsigned int num_providers; + + } success; + + struct + { + /** + * Array of URLs with requested payments. + */ + struct ANASTASIS_SharePaymentRequest *payment_requests; + + /** + * Length of the payment_requests array. + */ + unsigned int payment_requests_length; + } payment_required; + + struct + { + /** + * Base URL of the failed provider. + */ + const char *provider_url; + + /** + * HTTP status returned by the provider. + */ + unsigned int http_status; + + /** + * Upload status of the provider. + */ + enum ANASTASIS_UploadStatus ec; + + + } provider_failure; + + } details; + +}; + + +/** + * Function called with the results of a #ANASTASIS_secret_share(). + * + * @param cls closure + * @param sr share result + */ +typedef void +(*ANASTASIS_ShareResultCallback)(void *cls, + const struct ANASTASIS_ShareResult *sr); + + +/** + * Defines a recovery document upload process (recovery document + * consists of multiple policies) + */ +struct ANASTASIS_SecretShare; + + +/** + * Details of a past payment + */ +struct ANASTASIS_ProviderDetails +{ + /** + * URL of the provider backend. + */ + const char *provider_url; + + /** + * Payment order ID / secret of a past payment. + */ + struct ANASTASIS_PaymentSecretP payment_secret; + + /** + * Server salt. Points into a truth object from which we got the + * salt. + */ + struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt; +}; + + +/** + * Creates a recovery document with the created policies and uploads it to + * all servers. + * + * @param ctx the CURL context used to connect to the backend + * @param id_data used to create a account identifier on the escrow provider + * @param providers array of providers with URLs to upload the policies to + * @param pss_length length of the @a providers array + * @param policies list of policies which are included in this recovery document + * @param policies_length length of the @a policies array + * @param payment_years_requested for how many years would the client like the service to store the truth? + * @param pay_timeout how long to wait for payment + * @param spc payment callback is opened to pay the upload + * @param spc_cls closure for the @a spc payment callback + * @param src callback for the upload process + * @param src_cls closure for the @a src upload callback + * @param secret_name name of the core secret + * @param core_secret input of the user which is secured by anastasis e.g. (wallet private key) + * @param core_secret_size size of the @a core_secret + * @return NULL on error + */ +struct ANASTASIS_SecretShare * +ANASTASIS_secret_share (struct GNUNET_CURL_Context *ctx, + const json_t *id_data, + const struct ANASTASIS_ProviderDetails providers[], + unsigned int pss_length, + const struct ANASTASIS_Policy *policies[], + unsigned int policies_len, + uint32_t payment_years_requested, + struct GNUNET_TIME_Relative pay_timeout, + ANASTASIS_ShareResultCallback src, + void *src_cls, + const char *secret_name, + const void *core_secret, + size_t core_secret_size); + + +/** + * Cancels a secret share request. + * + * @param ss handle to the request + */ +void +ANASTASIS_secret_share_cancel (struct ANASTASIS_SecretShare *ss); + + +#endif diff --git a/src/include/anastasis_authorization_lib.h b/src/include/anastasis_authorization_lib.h new file mode 100644 index 0000000..80740be --- /dev/null +++ b/src/include/anastasis_authorization_lib.h @@ -0,0 +1,52 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_authorization_lib.h + * @brief database plugin loader + * @author Dominik Meister + * @author Dennis Neufeld + * @author Christian Grothoff + */ +#ifndef ANASTASIS_AUTHORIZATION_LIB_H +#define ANASTASIS_AUTHORIZATION_LIB_H + +#include "anastasis_authorization_plugin.h" + +/** + * Load authorization plugin. + * + * @param method name of the method to load + * @param AH_cfg configuration to use + * @param[out] set to the cost for using the plugin during recovery + * @return #GNUNET_OK on success + */ +struct ANASTASIS_AuthorizationPlugin * +ANASTASIS_authorization_plugin_load ( + const char *method, + const struct GNUNET_CONFIGURATION_Handle *AH_cfg, + struct TALER_Amount *cost); + + +/** + * shutdown all loaded plugins. + * + * @param void + */ +void +ANASTASIS_authorization_plugin_shutdown (void); + +#endif +/* end of anastasis_authorization_lib.h */ diff --git a/src/include/anastasis_authorization_plugin.h b/src/include/anastasis_authorization_plugin.h new file mode 100644 index 0000000..5985f52 --- /dev/null +++ b/src/include/anastasis_authorization_plugin.h @@ -0,0 +1,183 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_authorization_plugin.h + * @brief authorization access for Anastasis + * @author Christian Grothoff + */ +#ifndef ANASTASIS_AUTHORIZATION_PLUGIN_H +#define ANASTASIS_AUTHORIZATION_PLUGIN_H + +#include "anastasis_service.h" +#include <taler/taler_util.h> + + +/** + * Plugin-specific state for an authorization operation. + */ +struct ANASTASIS_AUTHORIZATION_State; + + +/** + * Enumeration values indicating the various possible + * outcomes of the plugin's `process` function. + */ +enum ANASTASIS_AUTHORIZATION_Result +{ + /** + * We successfully sent the authorization challenge + * and queued a reply to MHD. + */ + ANASTASIS_AUTHORIZATION_RES_SUCCESS = 0, + + /** + * We failed to transmit the authorization challenge, + * but successfully queued a failure response to MHD. + */ + ANASTASIS_AUTHORIZATION_RES_FAILED = 1, + + /** + * The plugin suspended the MHD connection as it needs some more + * time to do its (asynchronous) work before we can proceed. The + * plugin will resume the MHD connection when its work is done, and + * then the `process` function should be called again. + */ + ANASTASIS_AUTHORIZATION_RES_SUSPENDED = 2, + + /** + * The plugin tried to queue a reply on the MHD connection and + * failed to do so. We should return #MHD_NO to MHD to cause the + * HTTP connection to be closed without any reply. + * + * However, we were successful at transmitting the challenge, + * so the challenge should be marked as sent. + */ + ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED = 4, + + /** + * The plugin tried to queue a reply on the MHD connection and + * failed to do so. We should return #MHD_NO to MHD to cause the + * HTTP connection to be closed without any reply. + * + * Additionally, we failed to transmit the challenge. + */ + ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED = 5 +}; + + +/** + * Handle to interact with a authorization backend. + */ +struct ANASTASIS_AuthorizationPlugin +{ + + /** + * Closure for all callbacks. + */ + void *cls; + + /** + * How long should a generated challenge be valid for this type of method. + */ + struct GNUNET_TIME_Relative code_validity_period; + + /** + * How long before we should rotate a challenge for this type of method. + */ + struct GNUNET_TIME_Relative code_rotation_period; + + /** + * How long before we should retransmit a code. + */ + struct GNUNET_TIME_Relative code_retransmission_frequency; + + /** + * 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 + */ + enum GNUNET_GenericReturnValue + (*validate)(void *cls, + struct MHD_Connection *connection, + const char *truth_mime, + const char *data, + size_t data_length); + + + /** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification, + * or at least setup our authorization state (actual processing + * may also be startedin the @e process function). + * + * @param cls closure + * @param trigger function to call when we made progress + * @param trigger_cls closure for @a trigger + * @param truth_public_key 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 auth_command authentication command which is executed + * @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 + */ + struct ANASTASIS_AUTHORIZATION_State * + (*start)(void *cls, + GNUNET_SCHEDULER_TaskCallback trigger, + void *trigger_cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_public_key, + uint64_t code, + const void *data, + size_t data_length); + + + /** + * Continue issuing authentication challenge to user based on @a data. + * I.e. check if the transmission of the challenge via SMS or e-mail + * has completed and/or manipulate @a connection to redirect the client + * to a video identification site. + * + * @param as authorization state + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ + enum ANASTASIS_AUTHORIZATION_Result + (*process)(struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection); + + + /** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ + void + (*cleanup)(struct ANASTASIS_AUTHORIZATION_State *as); + +}; +#endif diff --git a/src/include/anastasis_crypto_lib.h b/src/include/anastasis_crypto_lib.h new file mode 100644 index 0000000..bf29b27 --- /dev/null +++ b/src/include/anastasis_crypto_lib.h @@ -0,0 +1,533 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/anastasis_cryto_lib.h + * @brief anastasis crypto api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include <jansson.h> +#include <gnunet/gnunet_crypto_lib.h> + + +/** + * Server to client: this is the policy version. + */ +#define ANASTASIS_HTTP_HEADER_POLICY_VERSION "Anastasis-Version" + +/** + * Server to client: this is the policy expiration time. + */ +#define ANASTASIS_HTTP_HEADER_POLICY_EXPIRATION "Anastasis-Policy-Expiration" + +/** + * Client to server: use this to decrypt the truth. + */ +#define ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY \ + "Anastasis-Truth-Decryption-Key" + +/** + * Client to server: I paid using this payment secret. + */ +#define ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER "Anastasis-Payment-Identifier" + +/** + * Client to server: I am authorized to update this policy, or + * server to client: I prove this is a valid policy. + */ +#define ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE "Anastasis-Policy-Signature" + +/** + * Server to client: Taler Payto-URI. + */ +#define ANASTASIS_HTTP_HEADER_TALER "Taler" + + +GNUNET_NETWORK_STRUCT_BEGIN + +/** + * An EdDSA public key that is used to identify a user's account. + */ +struct ANASTASIS_CRYPTO_AccountPublicKeyP +{ + struct GNUNET_CRYPTO_EddsaPublicKey pub; +}; + + +/** + * An EdDSA private key that is used to identify a user's account. + */ +struct ANASTASIS_CRYPTO_AccountPrivateKeyP +{ + struct GNUNET_CRYPTO_EddsaPrivateKey priv; +}; + + +/** + * A UUID that is used to identify a truth object + */ +struct ANASTASIS_CRYPTO_TruthUUIDP +{ + struct GNUNET_ShortHashCode uuid; +}; + + +/** + * Specifies a TruthKey which is used to decrypt the Truth stored by the user. + */ +struct ANASTASIS_CRYPTO_TruthKeyP +{ + struct GNUNET_HashCode key GNUNET_PACKED; +}; + + +/** + * Specifies a salt value used to encrypt the master public key. + */ +struct ANASTASIS_CRYPTO_MasterSaltP +{ + struct GNUNET_HashCode salt GNUNET_PACKED; +}; + + +/** + * Specifies a salt value used for salting the answer to a security question. + */ +struct ANASTASIS_CRYPTO_QuestionSaltP +{ + struct GNUNET_CRYPTO_PowSalt pow_salt; +}; + + +/** + * Specifies a salt value provided by an Anastasis provider, + * used for deriving the provider-specific user ID. + */ +struct ANASTASIS_CRYPTO_ProviderSaltP +{ + struct GNUNET_CRYPTO_PowSalt salt; +}; + + +/** + * Specifies a policy key which is used to decrypt the master key + */ +struct ANASTASIS_CRYPTO_PolicyKeyP +{ + struct GNUNET_HashCode key GNUNET_PACKED; +}; + + +/** + * Specifies an encrypted master key, the key is used to encrypt the core secret from the user + */ +struct ANASTASIS_CRYPTO_EncryptedMasterKeyP +{ + struct GNUNET_HashCode key GNUNET_PACKED; +}; + + +/** + * Specifies a Nonce used for the AES encryption, here defined as 32Byte large. + */ +struct ANASTASIS_CRYPTO_NonceP +{ + uint32_t nonce[8]; +}; + + +/** + * Specifies an IV used for the AES encryption, here defined as 16Byte large. + */ +struct ANASTASIS_CRYPTO_IvP +{ + uint32_t iv[4]; +}; + + +/** + * Specifies an symmetric key used for the AES encryption, here defined as 32Byte large. + */ +struct ANASTASIS_CRYPTO_SymKeyP +{ + uint32_t key[8]; +}; + + +/** + * Specifies an AES Tag used for the AES authentication, here defined as 16 Byte large. + */ +struct ANASTASIS_CRYPTO_AesTagP +{ + uint32_t aes_tag[4]; +}; + + +/** + * Specifies a Key Share from an escrow provider, the combined + * keyshares generate the EscrowMasterKey which is used to decrypt the + * Secret from the user. + */ +struct ANASTASIS_CRYPTO_KeyShareP +{ + uint32_t key[8]; +}; + + +/** + * Specifies an encrypted KeyShare + */ +struct ANASTASIS_CRYPTO_EncryptedKeyShareP +{ + /** + * Nonce used for the symmetric encryption. + */ + struct ANASTASIS_CRYPTO_NonceP nonce; + + /** + * GCM tag to check authenticity. + */ + struct ANASTASIS_CRYPTO_AesTagP tag; + + /** + * The actual key share. + */ + struct ANASTASIS_CRYPTO_KeyShareP keyshare; +}; + + +/** + * The escrow master key is the key used to encrypt the user secret (MasterKey). + */ +struct ANASTASIS_CRYPTO_EscrowMasterKeyP +{ + uint32_t key[8]; +}; + + +/** + * The user identifier consists of user information and the server salt. It is used as + * entropy source to generate the account public key and the encryption keys. + */ +struct ANASTASIS_CRYPTO_UserIdentifierP +{ + struct GNUNET_HashCode hash GNUNET_PACKED; +}; + + +/** + * Random identifier used to later charge a payment. + */ +struct ANASTASIS_PaymentSecretP +{ + uint32_t id[8]; +}; + + +/** + * Data signed by the account public key of a sync client to + * authorize the upload of the backup. + */ +struct ANASTASIS_UploadSignaturePS +{ + /** + * Set to #TALER_SIGNATURE_ANASTASIS_POLICY_UPLOAD. + */ + struct GNUNET_CRYPTO_EccSignaturePurpose purpose; + + /** + * Hash of the new backup. + */ + struct GNUNET_HashCode new_recovery_data_hash; + +}; + + +/** + * Signature made with an account's public key. + */ +struct ANASTASIS_AccountSignatureP +{ + /** + * We use EdDSA. + */ + struct GNUNET_CRYPTO_EddsaSignature eddsa_sig; +}; + + +GNUNET_NETWORK_STRUCT_END + + +/** + * Hash a numerical answer to compute the hash value to be submitted + * to the server for verification. Useful for PINs and SMS-TANs and + * other numbers submitted for challenges. + * + * @param code the numeric value to hash + * @param[out] hashed_code the resulting hash value to submit to the Anastasis server + */ +void +ANASTASIS_hash_answer (uint64_t code, + struct GNUNET_HashCode *hashed_code); + + +/** + * Creates the UserIdentifier, it is used as entropy source for the + * encryption keys and for the public and private key for signing the + * data. + * + * @param id_data JSON encoded data, which contains the raw user secret + * @param server_salt salt from the server (escrow provider) + * @param[out] id reference to the id which was created + */ +void +ANASTASIS_CRYPTO_user_identifier_derive ( + const json_t *id_data, + const struct ANASTASIS_CRYPTO_ProviderSaltP *server_salt, + struct ANASTASIS_CRYPTO_UserIdentifierP *id); + + +/** + * Generates the eddsa public Key used as the account identifier on the providers + * + * @param id holds a hashed user secret which is used as entropy source for the public key generation + * @param[out] pub_key handle for the generated public key + */ +void +ANASTASIS_CRYPTO_account_public_key_derive ( + const struct ANASTASIS_CRYPTO_UserIdentifierP *id, + struct ANASTASIS_CRYPTO_AccountPublicKeyP *pub_key); + + +/** + * //FIXME combine these two + * Generates the eddsa public Key used as the account identifier on the providers + * + * @param id holds a hashed user secret which is used as entropy source for the public key generation + * @param[out] priv_key handle for the generated private key + */ +void +ANASTASIS_CRYPTO_account_private_key_derive ( + const struct ANASTASIS_CRYPTO_UserIdentifierP *id, + struct ANASTASIS_CRYPTO_AccountPrivateKeyP *priv_key); + + +/** + * Hash @a answer to security question with @a salt and @a uuid to compute + * @a result that would be sent to the service for authorization. + * + * @param answer human answer to a security question + * @param uuid the truth UUID (known to the service) + * @param salt random salt value, unknown to the service + * @param[out] result where to write the resulting hash + */ +void +ANASTASIS_CRYPTO_secure_answer_hash ( + const char *answer, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid, + const struct ANASTASIS_CRYPTO_QuestionSaltP *salt, + struct GNUNET_HashCode *result); + + +/** + * Encrypt and signs the recovery document with AES256, the recovery + * document is encrypted with a derivation from the user identifier + * and the salt "erd". + * + * @param id Hashed User input, used for the generation of the encryption key + * @param rec_doc contains the recovery document as raw data + * @param rd_size defines the size of the recovery document inside data + * @param[out] enc_rec_doc return from the result, which contains the encrypted recovery document + * and the nonce and iv used for the encryption as Additional Data + * @param[out] erd_size size of the result + */ +void +ANASTASIS_CRYPTO_recovery_document_encrypt ( + const struct ANASTASIS_CRYPTO_UserIdentifierP *id, + const void *rec_doc, + size_t rd_size, + void **enc_rec_doc, + size_t *erd_size); + + +/** + * Decrypts the recovery document with AES256, the decryption key is generated with + * the user identifier provided by the user and the salt "erd". The nonce and IV used for the encryption + * are the first 48Byte of the data. + * + * @param id Hashed User input, used for the generation of the encryption key + * @param enc_rec_doc, contains the encrypted recovery document and the nonce and iv used for the encryption. + * @param erd_size size of the data + * @param[out] rec_doc return from the result, which contains the encrypted recovery document + * and the nonce and iv used for the encryption as Additional Data + * @param[out] rd_size size of the result + */ +void +ANASTASIS_CRYPTO_recovery_document_decrypt ( + const struct ANASTASIS_CRYPTO_UserIdentifierP *id, + const void *enc_rec_doc, + size_t erd_size, + void **rec_doc, + size_t *rd_size); + + +/** + * Encrypts a keyshare with a key generated with the user identification as entropy and the salt "eks". + * + * @param key_share the key share which is afterwards encrypted + * @param id the user identification which is the entropy source for the key generation + * @param xsalt answer to security question, otherwise NULL; used as extra salt in KDF + * @param[out] enc_key_share holds the encrypted share, the first 48 Bytes are the used nonce and tag + */ +void +ANASTASIS_CRYPTO_keyshare_encrypt ( + const struct ANASTASIS_CRYPTO_KeyShareP *key_share, + const struct ANASTASIS_CRYPTO_UserIdentifierP *id, + const char *xsalt, + struct ANASTASIS_CRYPTO_EncryptedKeyShareP *enc_key_share); + + +/** + * Decrypts a keyshare with a key generated with the user identification as entropy and the salt "eks". + * + * @param enc_key_share holds the encrypted share, the first 48 Bytes are the used nonce and tag + * @param id the user identification which is the entropy source for the key generation + * @param xsalt answer to security question, otherwise NULL; used as extra salt in KDF + * @param[out] key_share the result of decryption + */ +void +ANASTASIS_CRYPTO_keyshare_decrypt ( + const struct ANASTASIS_CRYPTO_EncryptedKeyShareP *enc_key_share, + const struct ANASTASIS_CRYPTO_UserIdentifierP *id, + const char *xsalt, + struct ANASTASIS_CRYPTO_KeyShareP *key_share); + + +/** + * Encrypts the truth data which contains the hashed answer or the + * phone number. It is encrypted with AES256, the key is generated + * with the user identification as entropy source and the salt "ect". + * + * @param nonce value to use for the nonce + * @param truth_enc_key master key used for encryption of the truth (see interface EscrowMethod) + * @param truth truth which will be encrypted + * @param truth_size size of the truth + * @param[out] enc_truth return from the result, which contains the encrypted truth + * and the nonce and iv used for the encryption as Additional Data + * @param[out] ect_size size of the result + */ +void +ANASTASIS_CRYPTO_truth_encrypt ( + const struct ANASTASIS_CRYPTO_NonceP *nonce, + const struct ANASTASIS_CRYPTO_TruthKeyP *truth_enc_key, + const void *truth, + size_t truth_size, + void **enc_truth, + size_t *ect_size); + + +/** + * Decrypts the truth data which contains the hashed answer or the phone number.. + * It is decrypted with AES256, the key is generated with the user identification as + * entropy source and the salt "ect". + * + * @param truth_enc_key master key used for encryption of the truth (see interface EscrowMethod) + * @param enc_truth truth holds the encrypted truth which will be decrypted + * @param ect_size size of the truth data + * @param truth return from the result, which contains the truth + * @param truth_size size of the result + */ +void +ANASTASIS_CRYPTO_truth_decrypt ( + const struct ANASTASIS_CRYPTO_TruthKeyP *truth_enc_key, + const void *enc_truth, + size_t ect_size, + void **truth, + size_t *truth_size); + + +/** + * A key share is randomly generated, one key share is generated for every + * truth a policy contains. + * + * @param key_share[out] reference to the created key share. + */ +void +ANASTASIS_CRYPTO_keyshare_create ( + struct ANASTASIS_CRYPTO_KeyShareP *key_share); + + +/** + * Once per policy a policy key is derived. The policy key consists of + * multiple key shares which are combined and hashed. + * + * @param key_shares list of key shares which are combined + * @param keyshare_length amount of key shares inside the array + * @param salt salt value + * @param[out] policy_key reference to the created key + */ +void +ANASTASIS_CRYPTO_policy_key_derive ( + const struct ANASTASIS_CRYPTO_KeyShareP *key_shares, + unsigned int keyshare_length, + const struct ANASTASIS_CRYPTO_MasterSaltP *salt, + struct ANASTASIS_CRYPTO_PolicyKeyP *policy_key); + + +/** + * The core secret is the user provided secret which will be saved with Anastasis. + * The secret will be encrypted with the master key, the master key is a random key which will + * be generated. The master key afterwards will be encrypted with the different policy keys. + * Encryption is performed with AES256 + * + * @param policy_keys an array of policy keys which are used to encrypt the master key + * @param policy_keys_length defines the amount of policy keys and also the amount of encrypted master keys + * @param core_secret the user provided core secret which is secured by anastasis + * @param core_secret_size the size of the core secret + * @param[out] enc_core_secret the core secret is encrypted with the generated master key + * @param[out] encrypted_master_keys array of encrypted master keys which will be safed inside the policies one encrypted + * master key is created for each policy key + */ +void +ANASTASIS_CRYPTO_core_secret_encrypt ( + const struct ANASTASIS_CRYPTO_PolicyKeyP *policy_keys, + unsigned int policy_keys_length, + const void *core_secret, + size_t core_secret_size, + void **enc_core_secret, + struct ANASTASIS_CRYPTO_EncryptedMasterKeyP *encrypted_master_keys); + + +/** + * Decrypts the core secret with the master key. First the master key is decrypted with the provided policy key. + * Afterwards the core secret is encrypted with the master key. The core secret is returned. + * + * @param encrypted_master_key master key for decrypting the core secret, is itself encrypted by the policy key + * @param policy_key built policy key which will decrypt the master key + * @param encrypted_core_secret the encrypted core secret from the user, will be encrypted with the policy key + * @param encrypted_core_secret_size size of the encrypted core secret + * @param[out] core_secret decrypted core secret will be returned + * @param[out] core_secret_size size of core secret + */ +void +ANASTASIS_CRYPTO_core_secret_recover ( + const struct ANASTASIS_CRYPTO_EncryptedMasterKeyP *encrypted_master_key, + const struct ANASTASIS_CRYPTO_PolicyKeyP *policy_key, + const void *encrypted_core_secret, + size_t encrypted_core_secret_size, + void **core_secret, + size_t *core_secret_size); diff --git a/src/include/anastasis_database_lib.h b/src/include/anastasis_database_lib.h new file mode 100644 index 0000000..4b33ea8 --- /dev/null +++ b/src/include/anastasis_database_lib.h @@ -0,0 +1,49 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_database_lib.h + * @brief database plugin loader + * @author Dominik Meister + * @author Dennis Neufeld + * @author Christian Grothoff + */ +#ifndef ANASTASIS_DB_LIB_H +#define ANASTASIS_DB_LIB_H + +#include "anastasis_database_plugin.h" + +/** + * Initialize the plugin. + * + * @param cfg configuration to use + * @return NULL on failure + */ +struct ANASTASIS_DatabasePlugin * +ANASTASIS_DB_plugin_load (const struct GNUNET_CONFIGURATION_Handle *cfg); + + +/** + * Shutdown the plugin. + * + * @param plugin plugin to unload + */ +void +ANASTASIS_DB_plugin_unload (struct ANASTASIS_DatabasePlugin *plugin); + + +#endif /* ANASTASIS_DB_LIB_H */ + +/* end of anastasis_database_lib.h */ diff --git a/src/include/anastasis_database_plugin.h b/src/include/anastasis_database_plugin.h new file mode 100644 index 0000000..488a5af --- /dev/null +++ b/src/include/anastasis_database_plugin.h @@ -0,0 +1,655 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_database_plugin.h + * @brief database access for Anastasis + * @author Christian Grothoff + */ +#ifndef ANASTASIS_DATABASE_PLUGIN_H +#define ANASTASIS_DATABASE_PLUGIN_H + +#include "anastasis_service.h" +#include <gnunet/gnunet_db_lib.h> + +/** + * How long is an offer for a challenge payment valid for payment? + */ +#define ANASTASIS_CHALLENGE_OFFER_LIFETIME GNUNET_TIME_UNIT_HOURS + +/** + * Return values for checking code validity. + */ +enum ANASTASIS_DB_CodeStatus +{ + /** + * Provided authentication code does not match database content. + */ + ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH = -3, + + /** + * Encountered hard error talking to DB. + */ + ANASTASIS_DB_CODE_STATUS_HARD_ERROR = -2, + + /** + * Encountered serialization error talking to DB. + */ + ANASTASIS_DB_CODE_STATUS_SOFT_ERROR = -1, + + /** + * We have no challenge in the database. + */ + ANASTASIS_DB_CODE_STATUS_NO_RESULTS = 0, + + /** + * The provided challenge matches what we have in the database. + */ + ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED = 1, +}; + + +/** + * Return values for checking account validity. + */ +enum ANASTASIS_DB_AccountStatus +{ + /** + * Account is unknown, user should pay to establish it. + */ + ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED = -3, + + /** + * Encountered hard error talking to DB. + */ + ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR = -2, + + /** + * Account is valid, but we have no policy stored yet. + */ + ANASTASIS_DB_ACCOUNT_STATUS_NO_RESULTS = 0, + + /** + * Account is valid, and we have a policy stored. + */ + ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED = 1, +}; + + +/** + * Return values for storing data in database with payment. + */ +enum ANASTASIS_DB_StoreStatus +{ + /** + * The client has stored too many policies, should pay to store more. + */ + ANASTASIS_DB_STORE_STATUS_STORE_LIMIT_EXCEEDED = -4, + + /** + * The client needs to pay to store policies. + */ + ANASTASIS_DB_STORE_STATUS_PAYMENT_REQUIRED = -3, + + /** + * Encountered hard error talking to DB. + */ + ANASTASIS_DB_STORE_STATUS_HARD_ERROR = -2, + + /** + * Despite retrying, we encountered serialization errors. + */ + ANASTASIS_DB_STORE_STATUS_SOFT_ERROR = -1, + + /** + * Database did not need an update (document exists). + */ + ANASTASIS_DB_STORE_STATUS_NO_RESULTS = 0, + + /** + * We successfully stored the document. + */ + ANASTASIS_DB_STORE_STATUS_SUCCESS = 1, +}; + + +/** + * Function called on all pending payments for an account or challenge. + * + * @param cls closure + * @param timestamp for how long have we been waiting + * @param payment_secret payment secret / order id in the backend + * @param amount how much is the order for + */ +typedef void +(*ANASTASIS_DB_PaymentPendingIterator)( + void *cls, + struct GNUNET_TIME_Absolute timestamp, + const struct ANASTASIS_PaymentSecretP *payment_secret, + const struct TALER_Amount *amount); + + +/** + * Handle to interact with the database. + * + * Functions ending with "_TR" run their OWN transaction scope + * and MUST NOT be called from within a transaction setup by the + * caller. Functions ending with "_NT" require the caller to + * setup a transaction scope. Functions without a suffix are + * simple, single SQL queries that MAY be used either way. + */ +struct ANASTASIS_DatabasePlugin +{ + + /** + * Closure for all callbacks. + */ + void *cls; + + /** + * Name of the library which generated this plugin. Set by the + * plugin loader. + */ + char *library_name; + + /** + * Drop anastasis tables. Used for testcases. + * + * @param cls closure + * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure + */ + int + (*drop_tables) (void *cls); + + /** + * Function called to perform "garbage collection" on the + * database, expiring records we no longer require. Deletes + * all user records that are not paid up (and by cascade deletes + * the associated recovery documents). Also deletes expired + * truth and financial records older than @a fin_expire. + * + * @param cls closure + * @param expire_backups backups older than the given time stamp should be garbage collected + * @param expire_pending_payments payments still pending from since before + * this value should be garbage collected + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*gc)(void *cls, + struct GNUNET_TIME_Absolute expire, + struct GNUNET_TIME_Absolute expire_pending_payments); + + /** + * Do a pre-flight check that we are not in an uncommitted transaction. + * If we are, try to commit the previous transaction and output a warning. + * Does not return anything, as we will continue regardless of the outcome. + * + * @param cls the `struct PostgresClosure` with the plugin-specific state + */ + void + (*preflight) (void *cls); + + /** + * Check that the database connection is still up. + * + * @param pg connection to check + */ + void + (*check_connection) (void *cls); + + /** + * Roll back the current transaction of a database connection. + * + * @param cls the `struct PostgresClosure` with the plugin-specific state + * @return #GNUNET_OK on success + */ + void + (*rollback) (void *cls); + + /** + * Start a transaction. + * + * @param cls the `struct PostgresClosure` with the plugin-specific state + * @param name unique name identifying the transaction (for debugging), + * must point to a constant + * @return #GNUNET_OK on success + */ + int + (*start) (void *cls, + const char *name); + + /** + * Commit the current transaction of a database connection. + * + * @param cls the `struct PostgresClosure` with the plugin-specific state + * @return transaction status code + */ + enum GNUNET_DB_QueryStatus + (*commit)(void *cls); + + + /** + * Store encrypted recovery document. + * + * @param cls closure + * @param anastasis_pub public key of the user's account + * @param account_sig signature affirming storage request + * @param data_hash hash of @a data + * @param data contains encrypted_recovery_document + * @param data_size size of data blob + * @param payment_secret identifier for the payment, used to later charge on uploads + * @param[out] version set to the version assigned to the document by the database + * @return transaction status, 0 if upload could not be finished because @a payment_secret + * did not have enough upload left; HARD error if @a payment_secret is unknown, ... + */ + enum ANASTASIS_DB_StoreStatus + (*store_recovery_document)( + void *cls, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + const struct ANASTASIS_AccountSignatureP *account_sig, + const struct GNUNET_HashCode *data_hash, + const void *data, + size_t data_size, + const struct ANASTASIS_PaymentSecretP *payment_secret, + uint32_t *version); + + + /** + * Fetch recovery document for user according given version. + * + * @param cls closure + * @param anastasis_pub public key of the user's account + * @param version the version number of the policy the user requests + * @param[out] account_sig signature + * @param[out] recovery_data_hash hash of the current recovery data + * @param[out] data_size size of data blob + * @param[out] data blob which contains the recovery document + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*get_recovery_document)( + void *cls, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + uint32_t version, + struct ANASTASIS_AccountSignatureP *account_sig, + struct GNUNET_HashCode *recovery_data_hash, + size_t *data_size, + void **data); + + + /** + * Fetch latest recovery document for user. + * + * @param cls closure + * @param anastasis_pub public key of the user's account + * @param account_sig signature + * @param recovery_data_hash hash of the current recovery data + * @param[out] data_size set to size of @a data blob + * @param[out] data set to blob which contains the recovery document + * @param[out] version set to the version number of the policy being returned + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*get_latest_recovery_document)( + void *cls, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + struct ANASTASIS_AccountSignatureP *account_sig, + struct GNUNET_HashCode *recovery_data_hash, + size_t *data_size, + void **data, + uint32_t *version); + + + /** + * Upload Truth, which contains the Truth and the KeyShare. + * + * @param cls closure + * @param truth_uuid the identifier for the Truth + * @param key_share_data contains information of an EncryptedKeyShare + * @param method name of method + * @param nonce nonce used to compute encryption key for encrypted_truth + * @param aes_gcm_tag authentication tag of encrypted_truth + * @param encrypted_truth contains the encrypted Truth which includes the ground truth i.e. H(challenge answer), phonenumber, SMS + * @param encrypted_truth_size the size of the Truth + * @param truth_expiration time till the according data will be stored + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*store_truth)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const struct ANASTASIS_CRYPTO_EncryptedKeyShareP *key_share_data, + const char *mime_type, + const void *encrypted_truth, + size_t encrypted_truth_size, + const char *method, + struct GNUNET_TIME_Relative truth_expiration); + + + /** + * Get the encrypted truth to validate the challenge response + * + * @param cls closure + * @param truth_uuid the identifier for the Truth + * @param[out] truth contains the encrypted truth + * @param[out] truth_size size of the encrypted truth + * @param[out] truth_mime mime type of truth + * @param[out] method type of the challenge + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*get_escrow_challenge)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + void **truth, + size_t *truth_size, + char **truth_mime, + char **method); + + + /** + * Lookup (encrypted) key share by @a truth_uuid. + * + * @param cls closure + * @param truth_uuid the identifier for the Truth + * @param[out] key_share set to the encrypted Keyshare + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*get_key_share)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct ANASTASIS_CRYPTO_EncryptedKeyShareP *key_share); + + + /** + * Check if an account exists, and if so, return the + * current @a recovery_document_hash. + * + * @param cls closure + * @param anastasis_pub account identifier + * @param[out] paid_until until when is the account paid up? + * @param[out] recovery_data_hash set to hash of @a recovery document + * @param[out] version set to the recovery policy version + * @return transaction status + */ + enum ANASTASIS_DB_AccountStatus + (*lookup_account)( + void *cls, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + struct GNUNET_TIME_Absolute *paid_until, + struct GNUNET_HashCode *recovery_data_hash, + uint32_t *version); + + + /** + * Check payment identifier. Used to check if a payment identifier given by + * the user is valid (existing and paid). + * + * @param cls closure + * @param payment_secret payment secret which the user must provide with every upload + * @param[out] paid bool value to show if payment is paid + * @param[out] valid_counter bool value to show if post_counter is > 0 + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*check_payment_identifier)( + void *cls, + const struct ANASTASIS_PaymentSecretP *payment_secret, + bool *paid, + bool *valid_counter); + + + /** + * Check payment identifier. Used to check if a payment identifier given by + * the user is valid (existing and paid). + * + * @param cls closure + * @param payment_secret payment secret which the user must provide with every upload + * @param truth_uuid unique identifier of the truth the user must satisfy the challenge + * @param[out] paid bool value to show if payment is paid + * @param[out] valid_counter bool value to show if post_counter is > 0 + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*check_challenge_payment)( + void *cls, + const struct ANASTASIS_PaymentSecretP *payment_secret, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + bool *paid); + + + /** + * Increment account lifetime by @a lifetime. + * + * @param cls closure + * @param account_pub which account received a payment + * @param payment_identifier proof of payment, must be unique and match pending payment + * @param lifetime for how long is the account now paid (increment) + * @param[out] paid_until set to the end of the lifetime after the operation + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*increment_lifetime)( + void *cls, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + const struct ANASTASIS_PaymentSecretP *payment_identifier, + struct GNUNET_TIME_Relative lifetime, + struct GNUNET_TIME_Absolute *paid_until); + + + /** + * Update account lifetime to the maximum of the current + * value and @a eol. + * + * @param cls closure + * @param account_pub which account received a payment + * @param payment_identifier proof of payment, must be unique and match pending payment + * @param eol for how long is the account now paid (absolute) + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*update_lifetime)( + void *cls, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + const struct ANASTASIS_PaymentSecretP *payment_identifier, + struct GNUNET_TIME_Absolute eol); + + + /** + * Store payment. Used to begin a payment, not indicative + * that the payment actually was made. (That is done + * when we increment the account's lifetime.) + * + * @param cls closure + * @param anastasis_pub anastasis's public key + * @param post_counter how many uploads does @a amount pay for + * @param payment_secret payment secret which the user must provide with every upload + * @param amount how much we asked for + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*record_recdoc_payment)( + void *cls, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + uint32_t post_counter, + const struct ANASTASIS_PaymentSecretP *payment_secret, + const struct TALER_Amount *amount); + + + /** + * Record truth upload payment was made. + * + * @param cls closure + * @param uuid the truth's UUID + * @param amount the amount that was paid + * @param duration how long is the truth paid for + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*record_truth_upload_payment)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid, + const struct TALER_Amount *amount, + struct GNUNET_TIME_Relative duration); + + + /** + * Inquire whether truth upload payment was made. + * + * @param cls closure + * @param uuid the truth's UUID + * @param[out] paid_until set for how long this truth is paid for + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*check_truth_upload_paid)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid, + struct GNUNET_TIME_Absolute *paid_until); + + + /** + * Verify the provided code with the code on the server. + * If the code matches the function will return with success, if the code + * does not match, the retry counter will be decreased by one. + * + * @param cls closure + * @param truth_pub identification of the challenge which the code corresponds to + * @param hashed_code code which the user provided and wants to verify + * @return transaction status + */ + enum ANASTASIS_DB_CodeStatus + (*verify_challenge_code)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_pub, + const struct GNUNET_HashCode *hashed_code); + + /** + * Insert a new challenge code for a given challenge identified by the challenge + * public key. The function will first check if there is already a valid code + * for this challenge present and won't insert a new one in this case. + * + * @param cls closure + * @param truth_uuid the identifier for the challenge + * @param rotation_period for how long is the code available + * @param validity_period for how long is the code available + * @param retry_counter amount of retries allowed + * @param[out] retransmission_date when to next retransmit + * @param[out] code set to the code which will be checked for later + * @return transaction status, + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if we are out of valid tries, + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if @a code is now in the DB + */ + enum GNUNET_DB_QueryStatus + (*create_challenge_code)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct GNUNET_TIME_Relative rotation_period, + struct GNUNET_TIME_Relative validity_period, + unsigned int retry_counter, + struct GNUNET_TIME_Absolute *retransmission_date, + uint64_t *code); + + + /** + * Remember in the database that we successfully sent a challenge. + * + * @param cls closure + * @param truth_uuid the identifier for the challenge + * @param code the challenge that was sent + */ + enum GNUNET_DB_QueryStatus + (*mark_challenge_sent)( + void *cls, + const struct ANASTASIS_PaymentSecretP *payment_secret, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code); + + + /** + * Store payment for challenge. + * + * @param cls closure + * @param truth_key identifier of the challenge to pay + * @param payment_secret payment secret which the user must provide with every upload + * @param amount how much we asked for + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*record_challenge_payment)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const struct ANASTASIS_PaymentSecretP *payment_secret, + const struct TALER_Amount *amount); + + + /** + * Record refund for challenge. + * + * @param cls closure + * @param truth_key identifier of the challenge to pay + * @param payment_secret payment secret which the user must provide with every upload + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*record_challenge_refund)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const struct ANASTASIS_PaymentSecretP *payment_secret); + + + /** + * Lookup for a pending payment for a certain challenge + * + * @param cls closure + * @param truth_uuid identification of the challenge + * @param[out] payment_secret set to the challenge payment secret + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*lookup_challenge_payment)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct ANASTASIS_PaymentSecretP *payment_secret); + + + /** + * Update payment status of challenge + * + * @param cls closure + * @param truth_uuid which challenge received a payment + * @param payment_identifier proof of payment, must be unique and match pending payment + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*update_challenge_payment)( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const struct ANASTASIS_PaymentSecretP *payment_identifier); + + + /** + * Function called to remove all expired codes from the database. + * FIXME: maybe implement as part of @e gc() in the future. + * + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*challenge_gc)(void *cls); + + +}; +#endif diff --git a/src/include/anastasis_json.h b/src/include/anastasis_json.h new file mode 100644 index 0000000..9e8d924 --- /dev/null +++ b/src/include/anastasis_json.h @@ -0,0 +1,410 @@ +/* + This file is part of Anastasis + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/anastasis_json.h + * @brief anastasis de-/serialization api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#ifndef ANASTASIS_JSON_H +#define ANASTASIS_JSON_H + +#include <jansson.h> +#include <gnunet/gnunet_util_lib.h> +#include "anastasis_error_codes.h" + +/** + * Enumeration of possible backup process status. + */ +enum ANASTASIS_BackupStatus +{ + ANASTASIS_BS_INITIAL, + ANASTASIS_BS_SELECT_CONTINENT, + ANASTASIS_BS_SELECT_COUNTRY, + ANASTASIS_BS_ENTER_USER_ATTRIBUTES, + ANASTASIS_BS_ADD_AUTHENTICATION_METHOD, + ANASTASIS_BS_ADD_POLICY, + ANASTASIS_BS_PAY +}; + +/** + * Enumeration of possible recovery process status. + */ +enum ANASTASIS_RecoveryStatus +{ + ANASTASIS_RS_INITIAL, + ANASTASIS_RS_SELECT_CONTINENT, + ANASTASIS_RS_SELECT_COUNTRY, + ANASTASIS_RS_ENTER_USER_ATTRIBUTES, + ANASTASIS_RS_SOLVE_CHALLENGE +}; + +// A state for the backup process. +struct ANASTASIS_BackupState +{ + enum ANASTASIS_BackupStatus status; + + union + { + + struct + { + // empty! + } select_continent; + + struct + { + const char *continent; + } select_country; + + struct + { + const char *continent; + const char *country; + const char *currency; // derived or per manual override! + json_t *user_attributes; + } enter_attributes; + + struct + { + const char *continent; + const char *country; + const char *currency; + json_t *user_attributes; + + struct AuthenticationDetails + { + enum AuthenticationMethod + { + SMS, + VIDEO, + SECQUEST, + EMAIL, + SNAILMAIL + }; + char *provider_url; + union Truth + { + + struct + { + char *phone_number; + } sms; + + struct + { + char *question; + char *answer; // FIXME: Reasonable to store answer in clear text here? + } secquest; + + struct + { + char *mailaddress; + } email; + + struct + { + char *full_name; + char *street; // street name + number + char *postal_code; + char *city; + char *country; + } snailmail; + + struct + { + char *path_to_picture; + } video; + } truth; + }*ad; // array + size_t ad_length; + } add_authentication; + struct + { + const char *continent; + const char *country; + const char *currency; + json_t *user_attributes; + + struct AuthenticationDetails + { + enum AuthenticationMethod + { + SMS, + VIDEO, + SECQUEST, + EMAIL, + SNAILMAIL + }; + char *provider_url; + union Truth + { + + struct + { + char *phone_number; + } sms; + + struct + { + char *question; + char *answer; // FIXME: Reasonable to store answer in clear text here? + } secquest; + + struct + { + char *mailaddress; + } email; + + struct + { + char *full_name; + char *street; // street name + number + char *postal_code; + char *city; + char *country; + } snailmail; + + struct + { + char *path_to_picture; + } video; + } truth; + }*ad; // array + size_t ad_length; + + struct PolicyDetails + { + struct AuthenticationDetails *ad; // array + }*pd; // array + size_t pd_length; + } add_policy; + // FIXME: add_payment + } details; +}; + + +// A state for the recovery process. +struct ANASTASIS_RecoveryState +{ + enum ANASTASIS_RecoveryStatus status; + + struct + { + // empty! + } select_continent; + + struct + { + const char *continent; + } select_country; + + struct + { + const char *continent; + const char *country; + const char *currency; // derived or per manual override! + json_t *user_attributes; + } enter_attributes; + + struct + { + const char *continent; + const char *country; + const char *currency; + json_t *user_attributes; + + struct ChallengeDetails + { + enum AuthenticationMethod + { + SMS, + VIDEO, + SECQUEST, + EMAIL, + SNAILMAIL + }; + char *provider_url; + union Challenge + { + + struct + { + char *phone_number; + char *code; + } sms; + + struct + { + char *question; + char *answer; // FIXME: Reasonable to store answer in clear text here? + } secquest; + + struct + { + char *mailaddress; + char *code; + } email; + + struct + { + char *full_name; + char *street; // street name + number + char *postal_code; + char *city; + char *country; + char *code; + } snailmail; + + struct + { + char *path_to_picture; + char *code; + } video; + } truth; + }*cd; // array + size_t cd_length; + } solve_challenge; +}; + +/** + * Definition of actions on ANASTASIS_BackupState. + */ +struct ANASTASIS_BackupAction +{ + enum action + { + ANASTASIS_BA_GET_SELECT_CONTINENT, + ANASTASIS_BA_GET_SELECT_COUNTRY, + ANASTASIS_BA_GET_ENTER_USER_ATTRIBUTES, + ANASTASIS_BA_GET_ADD_AUTHENTICATION_METHOD, + ANASTASIS_BA_GET_ADD_POLICY, + ANASTASIS_BA_GET_PAY, + ANASTASIS_BA_SET_SELECT_CONTINENT, + ANASTASIS_BA_SET_SELECT_COUNTRY, + ANASTASIS_BA_SET_ENTER_USER_ATTRIBUTES, + ANASTASIS_BA_SET_ADD_AUTHENTICATION_METHOD, + ANASTASIS_BA_SET_ADD_POLICY, + ANASTASIS_BA_SET_PAY + }; +}; + +/** + * Definition of actions on ANASTASIS_RecoveryState. + */ +struct ANASTASIS_RecoveryAction +{ + enum action + { + ANASTASIS_RS_GET_SELECT_CONTINENT, + ANASTASIS_RS_GET_SELECT_COUNTRY, + ANASTASIS_RS_GET_ENTER_USER_ATTRIBUTES, + ANASTASIS_RS_GET_SOLVE_CHALLENGE, + ANASTASIS_RS_SET_SELECT_CONTINENT, + ANASTASIS_RS_SET_SELECT_COUNTRY, + ANASTASIS_RS_SET_ENTER_USER_ATTRIBUTES, + ANASTASIS_RS_SET_SOLVE_CHALLENGE + }; +}; + + +/** + * Signature of the callback bassed to #ANASTASIS_apply_anastasis_backup_action + * for asynchronous actions on a #ANASTASIS_BackupState. + * + * @param cls closure + * @param new_bs the new #ANASTASIS_BackupState + * @param error error code + */ +typedef void +(*ANASTASIS_BackupApplyActionCallback)( + void *cls, + const struct ANASTASIS_BackupState *new_bs, + enum TALER_ErrorCode error); + + +/** + * Signature of the callback bassed to #ANASTASIS_apply_anastasis_recovery_action + * for asynchronous actions on a #ANASTASIS_RecoveryState. + * + * @param cls closure + * @param new_bs the new #ANASTASIS_RecoveryState + * @param error error code + */ +typedef void +(*ANASTASIS_RecoveryApplyActionCallback)( + void *cls, + const struct ANASTASIS_RecoveryState *new_rs, + enum TALER_ErrorCode error); + + +/** + * Returns an initial ANASTASIS_BackupState. + * + * @return initial ANASTASIS_BackupState + */ +struct ANASTASIS_BackupState * +ANASTASIS_get_initial_backup_state (); + + +/** + * Returns an initial ANASTASIS_RecoveryState. + * + * @return initial ANASTASIS_RecoveryState + */ +struct ANASTASIS_RecoveryState * +ANASTASIS_get_initial_recovery_state (); + + +/** + * Operates on a backup state depending on given #ANASTASIS_BackupState + * and #ANASTASIS_BackupAction. The new #ANASTASIS_BackupState is returned + * by a callback function. + * This function can do network access to talk to anastasis service providers. + * + * @param ctx url context for the event loop + * @param bs the previous *ANASTASIS_BackupState + * @param ba the action to do on #ANASTASIS_BackupState + * @param cb callback function to call with the action + */ +void +ANASTASIS_apply_anastasis_backup_action ( + struct GNUNET_CURL_Context *ctx, + struct ANASTASIS_BackupState *bs, + struct ANASTASIS_BackupAction *ba, + ANASTASIS_BackupApplyActionCallback cb); + + +/** + * Operates on a recovery state depending on given #ANASTASIS_RecoveryState + * and #ANASTASIS_RecoveryAction. The new #ANASTASIS_RecoveryState is returned + * by a callback function. + * This function can do network access to talk to anastasis service providers. + * + * @param ctx url context for the event loop + * @param bs the previous *ANASTASIS_RecoveryState + * @param ba the action to do on #ANASTASIS_RecoveryState + * @param cb callback function to call with the action + */ +void +ANASTASIS_apply_anastasis_recovery_action ( + struct GNUNET_CURL_Context *ctx, + struct ANASTASIS_RecoveryState *rs, + struct ANASTASIS_RecoveryAction *ra, + ANASTASIS_RecoveryApplyActionCallback cb); + +#endif /* _ANASTASIS_JSON_H */
\ No newline at end of file diff --git a/src/include/anastasis_redux.h b/src/include/anastasis_redux.h new file mode 100644 index 0000000..7a0ff53 --- /dev/null +++ b/src/include/anastasis_redux.h @@ -0,0 +1,127 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_redux.h + * @brief anastasis reducer api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#ifndef ANASTASIS_REDUX_H +#define ANASTASIS_REDUX_H + +#include <jansson.h> +#include "anastasis.h" +#include <taler/taler_mhd_lib.h> +#include <regex.h> + + +/** + * Initialize reducer subsystem. + * + * @param ctx context to use for CURL requests. + */ +void +ANASTASIS_redux_init (struct GNUNET_CURL_Context *ctx); + + +/** + * Terminate reducer subsystem. + */ +void +ANASTASIS_redux_done (void); + + +/** + * Returns an initial ANASTASIS backup state. + * + * @return NULL on failure + */ +json_t * +ANASTASIS_backup_start (const struct GNUNET_CONFIGURATION_Handle *cfg); + + +/** + * Returns an initial ANASTASIS recovery state. + * + * @return NULL on failure + */ +json_t * +ANASTASIS_recovery_start (const struct GNUNET_CONFIGURATION_Handle *cfg); + + +/** + * Returns an initial ANASTASIS recovery state. + * + * @return NULL on failure + */ +json_t * +ANASTASIS_recovery_start (const struct GNUNET_CONFIGURATION_Handle *cfg); + + +/** + * Signature of the callback passed to #ANASTASIS_backup_action and + * #ANASTASIS_recover_action. + * + * @param cls closure + * @param error error code, #TALER_EC_NONE if @a new_bs is the new successful state + * @param new_state the new state of the operation (client should json_incref() to keep an alias) + */ +typedef void +(*ANASTASIS_ActionCallback)(void *cls, + enum TALER_ErrorCode error, + json_t *new_state); + + +/** + * Handle to an ongoing action. Only valid until the #ANASTASIS_ActionCallback is invoked. + */ +struct ANASTASIS_ReduxAction; + + +/** + * Operates on a state depending on given #ANASTASIS_BackupState + * or #ANASTASIS_RecoveryState and #ANASTASIS_BackupAction or + * #ANASTASIS_RecoveryAction. + * The new #ANASTASIS_BackupState or #ANASTASIS_RecoveryState is returned + * by a callback function. + * This function can do network access to talk to anastasis service providers. + * + * @param state input state + * @param action what action to perform + * @param arguments data for the @a action + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return failure state or new state + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_redux_action (const json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * Cancel ongoing redux action. + * + * @param ra action to cancel + */ +void +ANASTASIS_redux_action_cancel (struct ANASTASIS_ReduxAction *ra); + + +#endif /* _ANASTASIS_REDUX_H */ diff --git a/src/include/anastasis_service.h b/src/include/anastasis_service.h new file mode 100644 index 0000000..98ac490 --- /dev/null +++ b/src/include/anastasis_service.h @@ -0,0 +1,703 @@ +/* + This file is part of TALER + 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 Affero 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with + Anastasis; see the file COPYING.LIB. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_service.h + * @brief C interface of libanastasisrest, a C library to use merchant's HTTP API + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#ifndef ANASTASIS_SERVICE_H +#define ANASTASIS_SERVICE_H + +#include "anastasis_crypto_lib.h" +#include "anastasis_util_lib.h" +#include <gnunet/gnunet_curl_lib.h> +#include <jansson.h> + + +/** + * Anastasis authorization method configuration + */ +struct ANASTASIS_AuthorizationMethodConfig +{ + /** + * Type of the method, i.e. "question". + */ + const char *type; + + /** + * Fee charged for accessing key share using this method. + */ + struct TALER_Amount usage_fee; +}; + + +/** + * @brief Anastasis configuration data. + */ +struct ANASTASIS_Config +{ + /** + * Protocol version supported by the server. + */ + const char *version; + + /** + * Business name of the anastasis provider. + */ + const char *business_name; + + /** + * Currency used for payments by the server. + */ + const char *currency; + + /** + * Array of authorization methods supported by the server. + */ + const struct ANASTASIS_AuthorizationMethodConfig *methods; + + /** + * Length of the @e methods array. + */ + unsigned int methods_length; + + /** + * Maximum size of an upload in megabytes. + */ + uint32_t storage_limit_in_megabytes; + + /** + * Annual fee for an account / policy upload. + */ + struct TALER_Amount annual_fee; + + /** + * Fee for a truth upload. + */ + struct TALER_Amount truth_upload_fee; + + /** + * Maximum legal liability for data loss covered by the + * provider. + */ + struct TALER_Amount liability_limit; + + /** + * Server salt. + */ + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + +}; + + +/** + * Function called with the result of a /config request. + * Note that an HTTP status of #MHD_HTTP_OK is no guarantee + * that @a acfg is non-NULL. @a acfg is non-NULL only if + * the server provided an acceptable response. + * + * @param cls closure + * @param http_status the HTTP status + * @param acfg configuration obtained, NULL if we could not parse it + */ +typedef void +(*ANASTASIS_ConfigCallback)(void *cls, + unsigned int http_status, + const struct ANASTASIS_Config *acfg); + + +/** + * @brief A Config Operation Handle + */ +struct ANASTASIS_ConfigOperation; + + +/** + * Run a GET /config request against the Anastasis backend. + * + * @param ctx CURL context to use + * @param base_url base URL fo the Anastasis backend + * @param cb function to call with the results + * @param cb_cls closure for @a cb + * @return handle to cancel the operation + */ +struct ANASTASIS_ConfigOperation * +ANASTASIS_get_config (struct GNUNET_CURL_Context *ctx, + const char *base_url, + ANASTASIS_ConfigCallback cb, + void *cb_cls); + + +/** + * Cancel ongoing #ANASTASIS_get_config() request. + * + * @param co configuration request to cancel. + */ +void +ANASTASIS_config_cancel (struct ANASTASIS_ConfigOperation *co); + + +/****** POLICY API ******/ + + +/** + * Detailed results from the successful download. + */ +struct ANASTASIS_DownloadDetails +{ + /** + * Signature (already verified). + */ + struct ANASTASIS_AccountSignatureP sig; + + /** + * Hash over @e policy and @e policy_size. + */ + struct GNUNET_HashCode curr_policy_hash; + + /** + * The backup we downloaded. + */ + const void *policy; + + /** + * Number of bytes in @e backup. + */ + size_t policy_size; + + /** + * Policy version returned by the service. + */ + uint32_t version; +}; + + +/** + * Handle for a GET /policy operation. + */ +struct ANASTASIS_PolicyLookupOperation; + + +/** + * Callback to process a GET /policy request + * + * @param cls closure + * @param http_status HTTP status code for this request + * @param ec anastasis-specific error code + * @param obj the response body + */ +typedef void +(*ANASTASIS_PolicyLookupCallback) (void *cls, + unsigned int http_status, + const struct ANASTASIS_DownloadDetails *dd); + + +/** + * Does a GET /policy. + * + * @param ctx execution context + * @param backend_url base URL of the merchant backend + * @param anastasis_pub public key of the user's account + * @param cb callback which will work the response gotten from the backend + * @param cb_cls closure to pass to the callback + * @return handle for this operation, NULL upon errors + */ +struct ANASTASIS_PolicyLookupOperation * +ANASTASIS_policy_lookup ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + ANASTASIS_PolicyLookupCallback cb, + void *cb_cls); + + +/** + * Does a GET /policy for a specific version. + * + * @param ctx execution context + * @param backend_url base URL of the merchant backend + * @param anastasis_pub public key of the user's account + * @param cb callback which will work the response gotten from the backend + * @param cb_cls closure to pass to the callback + * @param version version of the policy to be requested + * @return handle for this operation, NULL upon errors + */ +struct ANASTASIS_PolicyLookupOperation * +ANASTASIS_policy_lookup_version ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + ANASTASIS_PolicyLookupCallback cb, + void *cb_cls, + unsigned int version); + + +/** + * Cancel a GET /policy request. + * + * @param plo cancel the policy lookup operation + */ +void +ANASTASIS_policy_lookup_cancel ( + struct ANASTASIS_PolicyLookupOperation *plo); + + +/** + * Handle for a POST /policy operation. + */ +struct ANASTASIS_PolicyStoreOperation; + + +/** + * High-level ways how an upload may conclude. + */ +enum ANASTASIS_UploadStatus +{ + /** + * Backup was successfully made. + */ + ANASTASIS_US_SUCCESS = 0, + + /** + * Account expired or payment was explicitly requested + * by the client. + */ + ANASTASIS_US_PAYMENT_REQUIRED, + + /** + * HTTP interaction failed, see HTTP status. + */ + ANASTASIS_US_HTTP_ERROR, + + /** + * We had an internal error (not sure this can happen, + * but reserved for HTTP 400 status codes). + */ + ANASTASIS_US_CLIENT_ERROR, + + /** + * Server had an internal error. + */ + ANASTASIS_US_SERVER_ERROR, + + /** + * Truth already exists. Not applicable for policy uploads. + */ + ANASTASIS_US_CONFLICTING_TRUTH +}; + + +/** + * Result of an upload. + */ +struct ANASTASIS_UploadDetails +{ + /** + * High level status of the upload operation. Determines @e details. + */ + enum ANASTASIS_UploadStatus us; + + /** + * HTTP status code. + */ + unsigned int http_status; + + /** + * Taler error code. + */ + enum TALER_ErrorCode ec; + + union + { + + struct + { + /** + * Hash of the stored recovery data, returned if + * @e us is #ANASTASIS_US_SUCCESS. + */ + const struct GNUNET_HashCode *curr_backup_hash; + + /** + * At what time is the provider set to forget this + * policy (because the account expires)? + */ + struct GNUNET_TIME_Absolute policy_expiration; + + /** + * Version number of the resulting policy. + */ + unsigned long long policy_version; + + } success; + + /** + * Details about required payment. + */ + struct + { + /** + * A taler://pay/-URI with a request to pay the annual fee for + * the service. Returned if @e us is #ANASTASIS_US_PAYMENT_REQUIRED. + */ + const char *payment_request; + + /** + * The payment secret (aka order ID) extracted from the @e payment_request. + */ + struct ANASTASIS_PaymentSecretP ps; + } payment; + + } details; +}; + + +/** + * Callback to process a POST /policy request + * + * @param cls closure + * @param http_status HTTP status code for this request + * @param obj the decoded response body + */ +typedef void +(*ANASTASIS_PolicyStoreCallback) (void *cls, + const struct ANASTASIS_UploadDetails *up); + + +/** + * Store policies, does a POST /policy/$ACCOUNT_PUB + * + * @param ctx the CURL context used to connect to the backend + * @param backend_url backend's base URL, including final "/" + * @param anastasis_priv private key of the user's account + * @param recovery_data policy data to be stored + * @param recovery_data_size number of bytes in @a recovery_data + * @param payment_years_requested for how many years would the client like the service to store the truth? + * @param paid_order_id payment identifier of last payment + * @param payment_timeout how long to wait for the payment, use + * #GNUNET_TIME_UNIT_ZERO to let the server pick + * @param cb callback processing the response from /policy + * @param cb_cls closure for cb + * @return handle for the operation + */ +struct ANASTASIS_PolicyStoreOperation * +ANASTASIS_policy_store ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const struct ANASTASIS_CRYPTO_AccountPrivateKeyP *anastasis_priv, + const void *recovery_data, + size_t recovery_data_size, + uint32_t payment_years_requested, + const struct ANASTASIS_PaymentSecretP *payment_secret, + struct GNUNET_TIME_Relative payment_timeout, + ANASTASIS_PolicyStoreCallback cb, + void *cb_cls); + + +/** + * Cancel a POST /policy request. + * + * @param pso the policy store operation to cancel + */ +void +ANASTASIS_policy_store_cancel ( + struct ANASTASIS_PolicyStoreOperation *pso); + + +/****** TRUTH API ******/ + + +/** + * Operational status. + */ +enum ANASTASIS_KeyShareDownloadStatus +{ + /** + * We got the encrypted key share. + */ + ANASTASIS_KSD_SUCCESS = 0, + + /** + * Payment is needed to proceed with the recovery. + */ + ANASTASIS_KSD_PAYMENT_REQUIRED, + + /** + * The provided answer was wrong or missing. Instructions for + * getting a good answer may be provided. + */ + ANASTASIS_KSD_INVALID_ANSWER, + + /** + * To answer the challenge, the client should be redirected to + * the given URL. + */ + ANASTASIS_KSD_REDIRECT_FOR_AUTHENTICATION, + + /** + * The provider had an error. + */ + ANASTASIS_KSD_SERVER_ERROR, + + /** + * The provider claims we made an error. + */ + ANASTASIS_KSD_CLIENT_FAILURE, + + /** + * The provider does not know this truth. + */ + ANASTASIS_KSD_TRUTH_UNKNOWN, + + /** + * Too many attempts to solve the challenge were made in a short + * time. Try again laster. + */ + ANASTASIS_KSD_RATE_LIMIT_EXCEEDED + +}; + + +/** + * Detailed results from the successful download. + */ +struct ANASTASIS_KeyShareDownloadDetails +{ + + /** + * Operational status. + */ + enum ANASTASIS_KeyShareDownloadStatus status; + + /** + * Anastasis URL that returned the @e status. + */ + const char *server_url; + + /** + * Details depending on @e status. + */ + union + { + + /** + * The encrypted key share (if @e status is #ANASTASIS_KSD_SUCCESS). + */ + struct ANASTASIS_CRYPTO_EncryptedKeyShareP eks; + + /** + * Response if the challenge still needs to be answered, and the + * instructions are provided inline (no redirection). + */ + struct + { + + /** + * HTTP status returned by the server. #MHD_HTTP_ALREADY_REPORTED + * if the server did already send the challenge to the user, + * #MHD_HTTP_FORBIDDEN if the answer was wrong (or missing). + */ + unsigned int http_status; + + /** + * Response with server-side reply containing instructions for the user + */ + const char *body; + + /** + * Content-type: mime type of @e body, NULL if server did not provide any. + */ + const char *content_type; + + /** + * Number of bytes in @e body. + */ + size_t body_size; + + } open_challenge; + + /** + * URL with instructions for the user to satisfy the challenge, if + * @e status is #ANASTASIS_KSD_REDIRECT_FOR_AUTHENTICATION. + */ + const char *redirect_url; + + /** + * Response with instructions for how to pay, if + * @e status is #ANASTASIS_KSD_PAYMENT_REQUIRED. + */ + struct + { + + /** + * "taler://pay" URL with details how to pay for the challenge. + */ + const char *taler_pay_uri; + + /** + * The order ID from @e taler_pay_uri. + */ + struct ANASTASIS_PaymentSecretP payment_secret; + + } payment_required; + + + /** + * Response with details about a server-side failure, if + * @e status is #ANASTASIS_KSD_SERVER_FAILURE, + * #ANASTASIS_KSD_CLIENT_FAILURE or #ANASTASIS_KSD_TRUTH_UNKNOWN. + */ + struct + { + + /** + * HTTP status returned by the server. + */ + unsigned int http_status; + + /** + * Taler-specific error code. + */ + enum TALER_ErrorCode ec; + + } server_failure; + + } details; +}; + + +/** + * Handle for a GET /truth operation. + */ +struct ANASTASIS_KeyShareLookupOperation; + + +/** + * Callback to process a GET /truth request + * + * @param cls closure + * @param http_status HTTP status code for this request + * @param kdd details about the key share + */ +typedef void +(*ANASTASIS_KeyShareLookupCallback) ( + void *cls, + const struct ANASTASIS_KeyShareDownloadDetails *kdd); + + +/** + * Does a GET /truth. + * + * @param ctx execution context + * @param backend_url base URL of the merchant backend + * @param truth_public_key identification of the Truth + * @param truth_key Key used to Decrypt the Truth on the Server + * @param payment_secret secret from the previously done payment NULL to trigger payment + * @param payment_timeout how long to wait for the payment, use + * #GNUNET_TIME_UNIT_ZERO to let the server pick + * @param hashed_answer hashed answer to the challenge + * @param cb callback which will work the response gotten from the backend + * @param cb_cls closure to pass to the callback + * @return handle for this operation, NULL upon errors + */ +struct ANASTASIS_KeyShareLookupOperation * +ANASTASIS_keyshare_lookup ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const struct ANASTASIS_CRYPTO_TruthKeyP *truth_key, + const struct ANASTASIS_PaymentSecretP *payment_secret, + struct GNUNET_TIME_Relative timeout, + const struct GNUNET_HashCode *hashed_answer, + ANASTASIS_KeyShareLookupCallback cb, + void *cb_cls); + + +/** + * Cancel a GET /truth request. + * + * @param tlo cancel the truth lookup operation + */ +void +ANASTASIS_keyshare_lookup_cancel ( + struct ANASTASIS_KeyShareLookupOperation *kslo); + + +/** + * Handle for a POST /truth operation. + */ +struct ANASTASIS_TruthStoreOperation; + + +/** + * Callback to process a POST /truth request + * + * @param cls closure + * @param obj the response body + */ +typedef void +(*ANASTASIS_TruthStoreCallback) (void *cls, + const struct ANASTASIS_UploadDetails *up); + + +/** + * Store Truth, does a POST /truth/$UUID + * + * @param ctx the CURL context used to connect to the backend + * @param backend_url backend's base URL, including final "/" + * @param uuid unique identfication of the Truth Upload + * @param prev_truth_data_hash hash of the previous truth upload, NULL for the first upload ever + * @param type type of the authorization method + * @param encrypted_keyshare key material to return to the client upon authorization + * @param truth_mime mime type of @e encrypted_truth (after decryption) + * @param encrypted_truth_size number of bytes in @e encrypted_truth + * @param encrypted_truth contains the @a type-specific authorization data + * @param payment_years_requested for how many years would the client like the service to store the truth? + * @param payment_timeout how long to wait for the payment, use + * #GNUNET_TIME_UNIT_ZERO to let the server pick + * @param cb callback processing the response from /truth + * @param cb_cls closure for cb + * @return handle for the operation + */ +struct ANASTASIS_TruthStoreOperation * +ANASTASIS_truth_store ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid, + const char *type, + const struct ANASTASIS_CRYPTO_EncryptedKeyShareP *encrypted_keyshare, + const char *truth_mime, + size_t encrypted_truth_size, + const void *encrypted_truth, + uint32_t payment_years_requested, + struct GNUNET_TIME_Relative payment_timeout, + ANASTASIS_TruthStoreCallback cb, + void *cb_cls); + + +/** + * Cancel a POST /truth request. + * + * @param tso the truth store operation + */ +void +ANASTASIS_truth_store_cancel ( + struct ANASTASIS_TruthStoreOperation *tso); + + +#endif /* _ANASTASIS_SERVICE_H */ diff --git a/src/include/anastasis_testing_lib.h b/src/include/anastasis_testing_lib.h new file mode 100644 index 0000000..a54e3ae --- /dev/null +++ b/src/include/anastasis_testing_lib.h @@ -0,0 +1,884 @@ +/* + This file is part of Anastasis + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_testing_lib.h + * @brief API for writing an interpreter to test Taler components + * @author Christian Grothoff <christian@grothoff.org> + * @author Dennis Neufeld + * @author Dominik Meister + */ +#ifndef ANASTASIS_TESTING_LIB_H +#define ANASTASIS_TESTING_LIB_H + +#include "anastasis.h" +#include <taler/taler_testing_lib.h> +#include <microhttpd.h> + +/* ********************* Helper functions ********************* */ + +#define ANASTASIS_FAIL() \ + do {GNUNET_break (0); return NULL; } while (0) + +/** + * Index used in #ANASTASIS_TESTING_get_trait_hash() for the current hash. + */ +#define ANASTASIS_TESTING_TRAIT_HASH_CURRENT 0 + +/** + * Index used in #SYNC_TESTING_get_trait_hash() for the previous hash. + */ +#define ANASTASIS_TESTING_TRAIT_HASH_PREVIOUS 1 + +/** + * Obtain a hash from @a cmd. + * + * @param cmd command to extract the number from. + * @param index the number's index number, #ANASTASIS_TESTING_TRAIT_HASH_CURRENT or + * #SYNC_TESTING_TRAIT_HASH_PREVIOUS + * @param[out] h set to the hash coming from @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_hash (const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct GNUNET_HashCode **h); + + +/** + * Offer a hash. + * + * @param index the number's index number. + * @param h the hash to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_hash (unsigned int index, + const struct GNUNET_HashCode *h); + + +/** + * Obtain a truth decryption key from @a cmd. + * + * @param cmd command to extract the public key from. + * @param index usually 0 + * @param[out] key set to the account public key used in @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_truth_key ( + const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_CRYPTO_TruthKeyP **key); + + +/** + * Offer an truth decryption key. + * + * @param index usually zero + * @param h the account_pub to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_truth_key ( + unsigned int index, + const struct ANASTASIS_CRYPTO_TruthKeyP *h); + + +/** + * Obtain an account public key from @a cmd. + * + * @param cmd command to extract the public key from. + * @param index usually 0 + * @param[out] pub set to the account public key used in @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_account_pub ( + const struct + TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP **pub); + + +/** + * Offer an account public key. + * + * @param index usually zero + * @param h the account_pub to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_account_pub ( + unsigned int index, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *h); + + +/** + * Obtain an account private key from @a cmd. + * + * @param cmd command to extract the number from. + * @param index must be 0 + * @param[out] priv set to the account private key used in @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_account_priv ( + const struct + TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_CRYPTO_AccountPrivateKeyP **priv); + + +/** + * Offer an account private key. + * + * @param index usually zero + * @param priv the account_priv to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_account_priv ( + unsigned int index, + const struct + ANASTASIS_CRYPTO_AccountPrivateKeyP *priv); + +/** + * Obtain an account public key from @a cmd. + * + * @param cmd command to extract the payment identifier from. + * @param index the payment identifier's index number. + * @param[out] payment_secret set to the payment secret coming from @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_payment_secret ( + const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_PaymentSecretP **payment_secret); + + +/** + * Offer a payment secret. + * + * @param index usually zero + * @param h the payment secret to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_payment_secret ( + unsigned int index, + const struct ANASTASIS_PaymentSecretP *h); + + +/** + * Obtain an truth UUID from @a cmd. + * + * @param cmd command to extract the number from. + * @param index the number's index number. + * @param[out] uuid set to the number coming from @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_truth_uuid ( + const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_CRYPTO_TruthUUIDP **uuid); + + +/** + * Offer a truth UUID. + * + * @param index the number's index number. + * @param uuid the UUID to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_truth_uuid ( + unsigned int index, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid); + + +/** + * Obtain an encrypted key share from @a cmd. + * + * @param cmd command to extract the number from. + * @param index the number's index number. + * @param[out] uuid set to the number coming from @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_eks ( + const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_CRYPTO_EncryptedKeyShareP **eks); + + +/** + * Offer an encrypted key share. + * + * @param index the number's index number. + * @param eks the encrypted key share to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_eks ( + unsigned int index, + const struct ANASTASIS_CRYPTO_EncryptedKeyShareP *eks); + + +/** + * Obtain a code from @a cmd. + * + * @param cmd command to extract the number from. + * @param index the number's index number. + * @param[out] code set to the number coming from @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_code ( + const struct TALER_TESTING_Command *cmd, + unsigned int index, + const char **code); + + +/** + * Offer a filename. + * + * @param index the number's index number. + * @param tpk the public key to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_code (unsigned int index, + const char *code); + + +/** + * Prepare the merchant execution. Create tables and check if + * the port is available. + * + * @param config_filename configuration filename. + * + * @return the base url, or NULL upon errors. Must be freed + * by the caller. + */ +char * +TALER_TESTING_prepare_merchant (const char *config_filename); + + +/** + * Start the merchant backend process. Assume the port + * is available and the database is clean. Use the "prepare + * merchant" function to do such tasks. + * + * @param config_filename configuration filename. + * + * @return the process, or NULL if the process could not + * be started. + */ +struct GNUNET_OS_Process * +TALER_TESTING_run_merchant (const char *config_filename, + const char *merchant_url); + + +/** + * Start the anastasis backend process. Assume the port + * is available and the database is clean. Use the "prepare + * anastasis" function to do such tasks. + * + * @param config_filename configuration filename. + * + * @return the process, or NULL if the process could not + * be started. + */ +struct GNUNET_OS_Process * +ANASTASIS_TESTING_run_anastasis (const char *config_filename, + const char *anastasis_url); + + +/** + * Prepare the anastasis execution. Create tables and check if + * the port is available. + * + * @param config_filename configuration filename. + * + * @return the base url, or NULL upon errors. Must be freed + * by the caller. + */ +char * +ANASTASIS_TESTING_prepare_anastasis (const char *config_filename); + + +/* ************** Specific interpreter commands ************ */ + + +/** + * Types of options for performing the upload. Used as a bitmask. + */ +enum ANASTASIS_TESTING_PolicyStoreOption +{ + /** + * Do everything by the book. + */ + ANASTASIS_TESTING_PSO_NONE = 0, + + /** + * Use random hash for previous upload instead of correct + * previous hash. + */ + ANASTASIS_TESTING_PSO_PREV_HASH_WRONG = 1, + + /** + * Request payment. + */ + ANASTASIS_TESTING_PSO_REQUEST_PAYMENT = 2, + + /** + * Reference payment order ID from linked previous upload. + */ + ANASTASIS_TESTING_PSO_REFERENCE_ORDER_ID = 4 + +}; + + +/** + * Make a "policy store" command. + * + * @param label command label + * @param anastasis_url base URL of the anastasis serving + * the policy store request. + * @param prev_upload reference to a previous upload we are + * supposed to update, NULL for none + * @param http_status expected HTTP status. + * @param pso policy store options + * @param recovery_data recovery data to post + * @param recovery_data_size size of recovery/policy data + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_policy_store ( + const char *label, + const char *anastasis_url, + const char *prev_upload, + unsigned int http_status, + enum ANASTASIS_TESTING_PolicyStoreOption pso, + const void *recovery_data, + size_t recovery_data_size); + + +/** + * Make the "policy lookup" command. + * + * @param label command label + * @param ANASTASIS_url base URL of the ANASTASIS serving + * the policy lookup request. + * @param http_status expected HTTP status. + * @param upload_ref reference to upload command + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_policy_lookup (const char *label, + const char *ANASTASIS_url, + unsigned int http_status, + const char *upload_ref); + + +/** + * Types of options for performing the upload. Used as a bitmask. + */ +enum ANASTASIS_TESTING_TruthStoreOption +{ + /** + * Do everything by the book. + */ + ANASTASIS_TESTING_TSO_NONE = 0, + + /** + * Re-use UUID of previous upload instead of creating a random one. + */ + ANASTASIS_TESTING_TSO_REFERENCE_UUID = 1, + + /** + * Explicitly request payment. + */ + ANASTASIS_TESTING_TSO_REQUEST_PAYMENT = 2, + + /** + * Reference payment order ID from linked previous upload. + */ + ANASTASIS_TESTING_TSO_REFERENCE_ORDER_ID = 4 + +}; + + +/** + * Make the "truth store" command. + * + * @param label command label + * @param anastasis_url base URL of the anastasis serving + * the truth store request. + * @param prev_upload reference to a previous upload to get a payment ID from + * @param method what authentication method is being used + * @param mime_type MIME type of @a truth_data + * @param truth_data_size number of bytes in @a truth_data + * @param truth_data recovery data to post /truth (in plaintext) + * @param tso flags + * @param http_status expected HTTP status. + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_truth_store (const char *label, + const char *anastasis_url, + const char *prev_upload, + const char *method, + const char *mime_type, + size_t truth_data_size, + const void *truth_data, + enum ANASTASIS_TESTING_TruthStoreOption tso, + unsigned int http_status); + + +/** + * Make the "truth store" command for a secure question. + * + * @param label command label + * @param anastasis_url base URL of the anastasis serving + * the truth store request. + * @param prev_upload reference to a previous upload to get a payment ID from + * @param answer the answer to the question + * @param tso flags + * @param http_status expected HTTP status. + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_truth_question ( + const char *label, + const char *anastasis_url, + const char *prev_upload, + const char *answer, + enum ANASTASIS_TESTING_TruthStoreOption tso, + unsigned int http_status); + + +/** + * Make the "keyshare lookup" command. + * + * @param label command label + * @param anastasis_url base URL of the ANASTASIS serving + * the keyshare lookup request. + * @param answer (response to challenge) + * @param payment_ref reference to the payment request + * @param upload_ref reference to upload command + * @param ksdd expected status + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_keyshare_lookup ( + const char *label, + const char *anastasis_url, + const char *answer, + const char *payment_ref, + const char *upload_ref, + int lookup_mode, + enum ANASTASIS_KeyShareDownloadStatus ksdd); + + +/** + * Obtain a salt from @a cmd. + * + * @param cmd command to extract the salt from. + * @param index the salt's index number. + * @param[out] s set to the salt coming from @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_salt ( + const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_CRYPTO_ProviderSaltP **s); + + +/** + * Offer an salt. + * + * @param index the salt's index number. + * @param u the salt to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_salt ( + unsigned int index, + const struct ANASTASIS_CRYPTO_ProviderSaltP *s); + + +/** + * Make the "/config" command. + * + * @param label command label + * @param anastasis_url base URL of the ANASTASIS serving + * the /config request. + * @param http_status expected HTTP status. + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_config (const char *label, + const char *anastasis_url, + unsigned int http_status); + +/* ********************* test truth upload ********************* */ + +/** + * Obtain a truth from @a cmd. + * + * @param cmd command to extract the truth from. + * @param index the index of the truth + * @param[out] t set to the truth coming from @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_truth (const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_Truth **t); + + +/** + * Offer a truth. + * + * @param index the truth's index number. + * @param t the truth to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_truth (unsigned int index, + const struct ANASTASIS_Truth *t); + +/** + * Creates a sample of id_data. + * + * @param id_data some sample data (e.g. AHV, name, surname, ...) + * @return truth in json format + */ +json_t * +ANASTASIS_TESTING_make_id_data_example (const char *id_data); + + +/** + * Make the "truth upload" command. + * + * @param label command label + * @param anastasis_url base URL of the anastasis serving our requests. + * @param id_data ID data to generate user identifier + * @param method specifies escrow method + * @param instructions specifies what the client/user has to do + * @param mime_type mime type of truth_data + * @param truth_data some truth data (e.g. hash of answer to a secret question) + * @param truth_data_size size of truth_data + * @param http_status expected HTTP status + * @param tso truth upload options + * @param upload_ref reference to the previous upload + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_truth_upload ( + const char *label, + const char *anastasis_url, + const json_t *id_data, + const char *method, + const char *instructions, + const char *mime_type, + const void *truth_data, + size_t truth_data_size, + unsigned int http_status, + enum ANASTASIS_TESTING_TruthStoreOption tso, + const char *upload_ref); + + +/** + * Make the "truth upload" command for a security question. + * + * @param label command label + * @param anastasis_url base URL of the anastasis serving our requests. + * @param id_data ID data to generate user identifier + * @param instructions specifies what the client/user has to do + * @param mime_type mime type of truth_data + * @param answer the answer to the security question + * @param http_status expected HTTP status + * @param tso truth upload options + * @param upload_ref reference to the previous upload + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_truth_upload_question ( + const char *label, + const char *anastasis_url, + const json_t *id_data, + const char *instructions, + const char *mime_type, + const void *answer, + unsigned int http_status, + enum ANASTASIS_TESTING_TruthStoreOption tso, + const char *salt_ref); + +/* ********************* test policy create ********************* */ + +/** + * Obtain a policy from @a cmd. + * + * @param cmd command to extract the policy from. + * @param index the index of the policy + * @param[out] p set to the policy coming from @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_policy (const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_Policy **p); + + +/** + * Offer a policy. + * + * @param index the policy's index number. + * @param t the policy to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_policy (unsigned int index, + const struct ANASTASIS_Policy *p); + + +/** + * Make the "policy create" command. + * + * @param label command label + * @param ... NULL-terminated list of truth upload commands + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_policy_create (const char *label, + ...); + + +/* ********************* test secret share ********************* */ + +/** + * Obtain the core secret from @a cmd. + * + * @param cmd command to extract the core secret from. + * @param index the index of the core secret (usually 0) + * @param[out] s set to the core secret coming from @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_core_secret (const struct + TALER_TESTING_Command *cmd, + unsigned int index, + const void **s); + + +/** + * Offer the core secret. + * + * @param index the core secret's index number (usually 0). + * @param s the core secret to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_core_secret (unsigned int index, + const void *s); + +/** + * Types of options for performing the secret sharing. Used as a bitmask. + */ +enum ANASTASIS_TESTING_SecretShareOption +{ + /** + * Do everything by the book. + */ + ANASTASIS_TESTING_SSO_NONE = 0, + + /** + * Request payment. + */ + ANASTASIS_TESTING_SSO_REQUEST_PAYMENT = 2, + + /** + * Reference payment order ID from linked previous upload. + */ + ANASTASIS_TESTING_SSO_REFERENCE_ORDER_ID = 4 + +}; + +/** + * Make the "secret share" command. + * + * @param label command label + * @param anastasis_url base URL of the anastasis serving our requests. + * @param config_ref reference to /config operation for @a anastasis_url + * @param prev_secret_share reference to a previous secret share command + * @param id_data ID data to generate user identifier + * @param core_secret core secret to backup/recover + * @param core_secret_size size of @a core_secret + * @param http_status expected HTTP status. + * @param sso secret share options + * @param ... NULL-terminated list of policy create commands + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_secret_share ( + const char *label, + const char *anastasis_url, + const char *config_ref, + const char *prev_secret_share, + const json_t *id_data, + const void *core_secret, + size_t core_secret_size, + unsigned int http_status, + enum ANASTASIS_TESTING_SecretShareOption sso, + ...); + + +/* ********************* test recover secret ********************* */ + +/** + * Types of options for performing the secret recovery. Used as a bitmask. + */ +enum ANASTASIS_TESTING_RecoverSecretOption +{ + /** + * Do everything by the book. + */ + ANASTASIS_TESTING_RSO_NONE = 0, + + /** + * Request payment. + */ + ANASTASIS_TESTING_RSO_REQUEST_PAYMENT = 2, + + /** + * Reference payment order ID from linked previous download. + */ + ANASTASIS_TESTING_RSO_REFERENCE_ORDER_ID = 4 + +}; + + +/** + * Make the "recover secret" command. + * + * @param label command label + * @param anastasis_url base URL of the anastasis serving our requests. + * @param id_data identfication data from the user + * @param version of the recovery document to download + * @param rso recover secret options + * @param download_ref salt download reference + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_recover_secret ( + const char *label, + const char *anastasis_url, + const json_t *id_data, + unsigned int version, + enum ANASTASIS_TESTING_RecoverSecretOption rso, + const char *download_ref, + const char *core_secret_ref); + + +/** + * Make "recover secret finish" command. + * + * @param label command label + * @param recover_label label of a "recover secret" command to wait for + * @param timeout how long to wait at most + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_recover_secret_finish ( + const char *label, + const char *recover_label, + struct GNUNET_TIME_Relative timeout); + + +/* ********************* test challenge answer ********************* */ +/** + * Obtain a challenge from @a cmd. + * + * @param cmd command to extract the challenge from. + * @param index the index of the challenge + * @param[out] c set to the challenge coming from @a cmd. + * @return #GNUNET_OK on success. + */ +int +ANASTASIS_TESTING_get_trait_challenge (const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_Challenge **c); + +/** + * Offer a challenge. + * + * @param index the challenge index number. + * @param c the challenge to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_challenge (unsigned int index, + const struct ANASTASIS_Challenge *r); + + +/** + * Create a "challenge start" command. Suitable for the "file" + * authorization plugin. + * + * @param label command label + * @param payment_ref reference to payment made for this challenge + * @param challenge_ref reference to the recovery process + * @param challenge_index defines the index of the trait to solve + * @param expected_cs expected reply type + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_challenge_start ( + const char *label, + const char *payment_ref, + const char *challenge_ref, + unsigned int challenge_index, + enum ANASTASIS_ChallengeStatus expected_cs); + + +/** + * Make the "challenge answer" command. + * + * @param label command label + * @param payment_ref reference to payment made for this challenge + * @param challenge_ref reference to the recovery process + * @param challenge_index defines the index of the trait to solve + * @param answer to the challenge + * @param expected_cs expected reply type + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_challenge_answer ( + const char *label, + const char *payment_ref, + const char *challenge_ref, + unsigned int challenge_index, + const char *answer, + unsigned int mode, + enum ANASTASIS_ChallengeStatus expected_cs); + + +#endif diff --git a/src/include/anastasis_util_lib.h b/src/include/anastasis_util_lib.h new file mode 100644 index 0000000..9515c20 --- /dev/null +++ b/src/include/anastasis_util_lib.h @@ -0,0 +1,82 @@ +/* + This file is part of Anastasis + Copyright (C) 2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file include/anastasis_util_lib.h + * @brief anastasis client api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#ifndef ANASTASIS_UTIL_LIB_H +#define ANASTASIS_UTIL_LIB_H + +#include "anastasis_error_codes.h" +#define GNU_TALER_ERROR_CODES_H 1 +#include <gnunet/gnunet_util_lib.h> +#include <taler/taler_util.h> + + +/** + * Return default project data used by Anastasis. + */ +const struct GNUNET_OS_ProjectData * +ANASTASIS_project_data_default (void); + + +/** + * Handle for the child management + */ +struct ANASTASIS_ChildWaitHandle; + +/** + * Defines a ANASTASIS_ChildCompletedCallback which is sent back + * upon death or completion of a child process. Used to trigger + * authentication commands. + * + * @param cls handle for the callback + * @param type type of the process + * @param exit_code status code of the process + * +*/ +typedef void +(*ANASTASIS_ChildCompletedCallback)(void *cls, + enum GNUNET_OS_ProcessStatusType type, + long unsigned int exit_code); + + +/** + * Starts the handling of the child processes. + * Function checks the status of the child process and sends back a + * ANASTASIS_ChildCompletedCallback upon completion/death of the child. + * + * @param proc child process which is monitored + * @param cb reference to the callback which is called after completion + * @param cb_cls closure for the callback + * @return ANASTASIS_ChildWaitHandle is returned + */ +struct ANASTASIS_ChildWaitHandle * +ANASTASIS_wait_child (struct GNUNET_OS_Process *proc, + ANASTASIS_ChildCompletedCallback cb, + void *cb_cls); + +/** + * Stop waiting on this child. + */ +void +ANASTASIS_wait_child_cancel (struct ANASTASIS_ChildWaitHandle *cwh); + + +#endif diff --git a/src/include/gettext.h b/src/include/gettext.h new file mode 100644 index 0000000..4585126 --- /dev/null +++ b/src/include/gettext.h @@ -0,0 +1,71 @@ +/* Convenience header for conditional use of GNU <libintl.h>. + Copyright Copyright (C) 1995-1998, 2000-2002 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published + by the Free Software Foundation; either version 3, or (at your option) + any later version. + + This program 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + USA. */ + +#ifndef _LIBGETTEXT_H +#define _LIBGETTEXT_H 1 + +/* NLS can be disabled through the configure --disable-nls option. */ +#if ENABLE_NLS + +/* Get declarations of GNU message catalog functions. */ +#include <libintl.h> + +#else + +/* Solaris /usr/include/locale.h includes /usr/include/libintl.h, which + chokes if dcgettext is defined as a macro. So include it now, to make + later inclusions of <locale.h> a NOP. We don't include <libintl.h> + as well because people using "gettext.h" will not include <libintl.h>, + and also including <libintl.h> would fail on SunOS 4, whereas <locale.h> + is GNUNET_OK. */ +#if defined(__sun) +#include <locale.h> +#endif + +/* Disabled NLS. + The casts to 'const char *' serve the purpose of producing warnings + for invalid uses of the value returned from these functions. + On pre-ANSI systems without 'const', the config.h file is supposed to + contain "#define const". */ +#define gettext(Msgid) ((const char *) (Msgid)) +#define dgettext(Domainname, Msgid) ((const char *) (Msgid)) +#define dcgettext(Domainname, Msgid, Category) ((const char *) (Msgid)) +#define ngettext(Msgid1, Msgid2, N) \ + ((N) == 1 ? (const char *) (Msgid1) : (const char *) (Msgid2)) +#define dngettext(Domainname, Msgid1, Msgid2, N) \ + ((N) == 1 ? (const char *) (Msgid1) : (const char *) (Msgid2)) +#define dcngettext(Domainname, Msgid1, Msgid2, N, Category) \ + ((N) == 1 ? (const char *) (Msgid1) : (const char *) (Msgid2)) +/* slight modification here to avoid warnings: generate GNUNET_NO code, + not even the cast... */ +#define textdomain(Domainname) +#define bindtextdomain(Domainname, Dirname) +#define bind_textdomain_codeset(Domainname, Codeset) ((const char *) (Codeset)) + +#endif + +/* A pseudo function call that serves as a marker for the automated + extraction of messages, but does not call gettext(). The run-time + translation is done at a different place in the code. + The argument, String, should be a literal string. Concatenated strings + and other string expressions won't work. + The macro's expansion is not parenthesized, so that it is suitable as + initializer for static 'char[]' or 'const char[]' variables. */ +#define gettext_noop(String) String + +#endif /* _LIBGETTEXT_H */ diff --git a/src/include/platform.h b/src/include/platform.h new file mode 100644 index 0000000..04a2dce --- /dev/null +++ b/src/include/platform.h @@ -0,0 +1,60 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015, 2016 GNUnet e.V. and INRIA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 <http://www.gnu.org/licenses/> +*/ + +/** + * @file include/platform.h + * @brief This file contains the includes and definitions which are used by the + * rest of the modules + * @author Sree Harsha Totakura <sreeharsha@totakura.in> + */ + +#ifndef PLATFORM_H_ +#define PLATFORM_H_ + +/* Include our configuration header */ +#ifndef HAVE_USED_CONFIG_H +# define HAVE_USED_CONFIG_H +# ifdef HAVE_CONFIG_H +# include "anastasis_config.h" +# endif +#endif + + +#if (GNUNET_EXTRA_LOGGING >= 1) +#define VERBOSE(cmd) cmd +#else +#define VERBOSE(cmd) do { break; } while (0) +#endif + +/* Include the features available for GNU source */ +#define _GNU_SOURCE + +/* Include GNUnet's platform file */ +#include <gnunet/platform.h> + +/* Do not use shortcuts for gcrypt mpi */ +#define GCRYPT_NO_MPI_MACROS 1 + +/* Do not use deprecated functions from gcrypt */ +#define GCRYPT_NO_DEPRECATED 1 + +/* Ignore MHD deprecations for now as we want to be compatible + to "ancient" MHD releases. */ +#define MHD_NO_DEPRECATION 1 + +#endif /* PLATFORM_H_ */ + +/* end of platform.h */ diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am new file mode 100644 index 0000000..07460d4 --- /dev/null +++ b/src/lib/Makefile.am @@ -0,0 +1,27 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include -I$(top_srcdir)/src/backend -I$(top_srcdir)/src/lib + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +lib_LTLIBRARIES = \ + libanastasis.la + +libanastasis_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined +libanastasis_la_SOURCES = \ + anastasis_backup.c \ + anastasis_recovery.c +libanastasis_la_LIBADD = \ + $(top_builddir)/src/util/libanastasisutil.la \ + $(top_builddir)/src/restclient/libanastasisrest.la \ + -ltalerutil \ + -ltalermerchant \ + -lgnunetjson \ + -lgnunetutil \ + -ljansson \ + -lz \ + $(XLIB) diff --git a/src/lib/anastasis_backup.c b/src/lib/anastasis_backup.c new file mode 100644 index 0000000..ea55e6a --- /dev/null +++ b/src/lib/anastasis_backup.c @@ -0,0 +1,979 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 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 <http://www.gnu.org/licenses/> +*/ +/** + * @brief anastasis client api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis.h" +#include <taler/taler_merchant_service.h> +#include <zlib.h> + + +struct ANASTASIS_Truth +{ + /** + * Identification of the truth. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + + /** + * Keyshare of this truth, used to generate policy keys + */ + struct ANASTASIS_CRYPTO_KeyShareP key_share; + + /** + * Nonce used for the symmetric encryption. + */ + struct ANASTASIS_CRYPTO_NonceP nonce; + + /** + * Key used to encrypt this truth + */ + struct ANASTASIS_CRYPTO_TruthKeyP truth_key; + + /** + * Server salt used to derive user identifier + */ + struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt; + + /** + * Server salt used to derive hash from security answer + */ + struct ANASTASIS_CRYPTO_QuestionSaltP salt; + + /** + * Url of the server + */ + char *url; + + /** + * Method used for this truth + */ + char *type; + + /** + * Instructions for the user to recover this truth. + */ + char *instructions; + + /** + * Mime type of the truth, NULL if not given. + */ + char *mime_type; + +}; + + +struct ANASTASIS_Truth * +ANASTASIS_truth_from_json (const json_t *json) +{ + struct ANASTASIS_Truth *t = GNUNET_new (struct ANASTASIS_Truth); + const char *url; + const char *type; + const char *instructions; + const char *mime_type = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("url", + &url), + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_string ("instructions", + &instructions), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("mime_type", + &mime_type)), + GNUNET_JSON_spec_fixed_auto ("uuid", + &t->uuid), + GNUNET_JSON_spec_fixed_auto ("nonce", + &t->nonce), + GNUNET_JSON_spec_fixed_auto ("key_share", + &t->key_share), + GNUNET_JSON_spec_fixed_auto ("truth_key", + &t->truth_key), + GNUNET_JSON_spec_fixed_auto ("salt", + &t->salt), + GNUNET_JSON_spec_fixed_auto ("provider_salt", + &t->provider_salt), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + GNUNET_free (t); + return NULL; + } + t->url = GNUNET_strdup (url); + t->type = GNUNET_strdup (type); + t->instructions = GNUNET_strdup (instructions); + if (NULL != mime_type) + t->mime_type = GNUNET_strdup (mime_type); + return t; +} + + +json_t * +ANASTASIS_truth_to_json (const struct ANASTASIS_Truth *t) +{ + return json_pack ( + "{s:o,s:o,s:o,s:o,s:o" + ",s:o,s:s,s:s,s:s,s:s?}", + "uuid", + GNUNET_JSON_from_data_auto (&t->uuid), + "key_share", + GNUNET_JSON_from_data_auto (&t->key_share), + "truth_key", + GNUNET_JSON_from_data_auto (&t->truth_key), + "salt", + GNUNET_JSON_from_data_auto (&t->salt), + "nonce", + GNUNET_JSON_from_data_auto (&t->nonce), + "provider_salt", + GNUNET_JSON_from_data_auto (&t->provider_salt), + "url", + t->url, + "type", + t->type, + "instructions", + t->instructions, + "mime_type", + t->mime_type); +} + + +struct ANASTASIS_TruthUpload +{ + + /** + * User identifier used for the keyshare encryption + */ + struct ANASTASIS_CRYPTO_UserIdentifierP id; + + /** + * CURL Context for the Post Request + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Callback which sends back the generated truth object later used to build the policy + */ + ANASTASIS_TruthCallback tc; + + /** + * Closure for the Callback + */ + void *tc_cls; + + /** + * Reference to the Truthstore Operation + */ + struct ANASTASIS_TruthStoreOperation *tso; + + /** + * The truth we are uploading. + */ + struct ANASTASIS_Truth *t; + +}; + + +/** + * Function called with the result of trying to upload truth. + * + * @param cls our `struct ANASTASIS_TruthUpload` + * @param ud details about the upload result + */ +static void +truth_store_callback (void *cls, + const struct ANASTASIS_UploadDetails *ud) +{ + struct ANASTASIS_TruthUpload *tu = cls; + + tu->tso = NULL; + tu->tc (tu->tc_cls, + tu->t, + ud); + tu->t = NULL; + ANASTASIS_truth_upload_cancel (tu); +} + + +struct ANASTASIS_TruthUpload * +ANASTASIS_truth_upload3 (struct GNUNET_CURL_Context *ctx, + const struct ANASTASIS_CRYPTO_UserIdentifierP *user_id, + struct ANASTASIS_Truth *t, + const void *truth_data, + size_t truth_data_size, + uint32_t payment_years_requested, + struct GNUNET_TIME_Relative pay_timeout, + ANASTASIS_TruthCallback tc, + void *tc_cls) +{ + struct ANASTASIS_TruthUpload *tu; + struct ANASTASIS_CRYPTO_EncryptedKeyShareP encrypted_key_share; + struct GNUNET_HashCode nt; + void *encrypted_truth; + size_t encrypted_truth_size; + + tu = GNUNET_new (struct ANASTASIS_TruthUpload); + tu->tc = tc; + tu->tc_cls = tc_cls; + tu->ctx = ctx; + tu->id = *user_id; + tu->tc = tc; + tu->tc_cls = tc_cls; + tu->t = t; + + if (0 == strcmp ("question", + t->type)) + { + char *answer; + + answer = GNUNET_strndup (truth_data, + truth_data_size); + ANASTASIS_CRYPTO_secure_answer_hash (answer, + &t->uuid, + &t->salt, + &nt); + ANASTASIS_CRYPTO_keyshare_encrypt (&t->key_share, + &tu->id, + answer, + &encrypted_key_share); + GNUNET_free (answer); + truth_data = &nt; + truth_data_size = sizeof (nt); + } + else + { + ANASTASIS_CRYPTO_keyshare_encrypt (&t->key_share, + &tu->id, + NULL, + &encrypted_key_share); + } + ANASTASIS_CRYPTO_truth_encrypt (&t->nonce, + &t->truth_key, + truth_data, + truth_data_size, + &encrypted_truth, + &encrypted_truth_size); + tu->tso = ANASTASIS_truth_store (tu->ctx, + t->url, + &t->uuid, + t->type, + &encrypted_key_share, + t->mime_type, + encrypted_truth_size, + encrypted_truth, + payment_years_requested, + pay_timeout, + &truth_store_callback, + tu); + GNUNET_free (encrypted_truth); + if (NULL == tu->tso) + { + GNUNET_break (0); + ANASTASIS_truth_free (t); + ANASTASIS_truth_upload_cancel (tu); + return NULL; + } + return tu; +} + + +struct ANASTASIS_TruthUpload * +ANASTASIS_truth_upload2 ( + struct GNUNET_CURL_Context *ctx, + const struct ANASTASIS_CRYPTO_UserIdentifierP *user_id, + const char *provider_url, + const char *type, + const char *instructions, + const char *mime_type, + const struct ANASTASIS_CRYPTO_ProviderSaltP *provider_salt, + const void *truth_data, + size_t truth_data_size, + uint32_t payment_years_requested, + struct GNUNET_TIME_Relative pay_timeout, + const struct ANASTASIS_CRYPTO_NonceP *nonce, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid, + const struct ANASTASIS_CRYPTO_QuestionSaltP *salt, + const struct ANASTASIS_CRYPTO_TruthKeyP *truth_key, + const struct ANASTASIS_CRYPTO_KeyShareP *key_share, + ANASTASIS_TruthCallback tc, + void *tc_cls) +{ + struct ANASTASIS_Truth *t; + + t = GNUNET_new (struct ANASTASIS_Truth); + t->url = GNUNET_strdup (provider_url); + t->type = GNUNET_strdup (type); + t->instructions = (NULL != instructions) + ? GNUNET_strdup (instructions) + : NULL; + t->mime_type = (NULL != mime_type) + ? GNUNET_strdup (mime_type) + : NULL; + t->provider_salt = *provider_salt; + t->salt = *salt; + t->nonce = *nonce; + t->uuid = *uuid; + t->truth_key = *truth_key; + t->key_share = *key_share; + return ANASTASIS_truth_upload3 (ctx, + user_id, + t, + truth_data, + truth_data_size, + payment_years_requested, + pay_timeout, + tc, + tc_cls); +} + + +struct ANASTASIS_TruthUpload * +ANASTASIS_truth_upload ( + struct GNUNET_CURL_Context *ctx, + const struct ANASTASIS_CRYPTO_UserIdentifierP *user_id, + const char *provider_url, + const char *type, + const char *instructions, + const char *mime_type, + const struct ANASTASIS_CRYPTO_ProviderSaltP *provider_salt, + const void *truth_data, + size_t truth_data_size, + uint32_t payment_years_requested, + struct GNUNET_TIME_Relative pay_timeout, + ANASTASIS_TruthCallback tc, + void *tc_cls) +{ + struct ANASTASIS_CRYPTO_QuestionSaltP question_salt; + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + struct ANASTASIS_CRYPTO_TruthKeyP truth_key; + struct ANASTASIS_CRYPTO_KeyShareP key_share; + struct ANASTASIS_CRYPTO_NonceP nonce; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &nonce, + sizeof (nonce)); + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &question_salt, + sizeof (question_salt)); + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &uuid, + sizeof (uuid)); + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + &truth_key, + sizeof (truth_key)); + ANASTASIS_CRYPTO_keyshare_create (&key_share); + return ANASTASIS_truth_upload2 (ctx, + user_id, + provider_url, + type, + instructions, + mime_type, + provider_salt, + truth_data, + truth_data_size, + payment_years_requested, + pay_timeout, + &nonce, + &uuid, + &question_salt, + &truth_key, + &key_share, + tc, + tc_cls); +} + + +void +ANASTASIS_truth_upload_cancel (struct ANASTASIS_TruthUpload *tu) +{ + if (NULL != tu->tso) + { + ANASTASIS_truth_store_cancel (tu->tso); + tu->tso = NULL; + } + if (NULL != tu->t) + { + ANASTASIS_truth_free (tu->t); + tu->t = NULL; + } + GNUNET_free (tu); +} + + +void +ANASTASIS_truth_free (struct ANASTASIS_Truth *t) +{ + GNUNET_free (t->url); + GNUNET_free (t->type); + GNUNET_free (t->instructions); + GNUNET_free (t->mime_type); + GNUNET_free (t); +} + + +struct ANASTASIS_Policy +{ + /** + * Encrypted policy master key + */ + struct ANASTASIS_CRYPTO_PolicyKeyP policy_key; + + /** + * Salt used to encrypt the master key + */ + struct ANASTASIS_CRYPTO_MasterSaltP salt; + + /** + * Array of truths + */ + struct ANASTASIS_Truth **truths; + + /** + * Length of @ truths array. + */ + uint32_t truths_length; + +}; + + +/** + * Duplicate truth object. + * + * @param t object to duplicate + * @return copy of @a t + */ +static struct ANASTASIS_Truth * +truth_dup (const struct ANASTASIS_Truth *t) +{ + struct ANASTASIS_Truth *d = GNUNET_new (struct ANASTASIS_Truth); + + *d = *t; + d->url = GNUNET_strdup (t->url); + d->type = GNUNET_strdup (t->type); + d->instructions = GNUNET_strdup (t->instructions); + if (NULL != t->mime_type) + d->mime_type = GNUNET_strdup (t->mime_type); + return d; +} + + +struct ANASTASIS_Policy * +ANASTASIS_policy_create (const struct ANASTASIS_Truth *truths[], + unsigned int truths_len) +{ + struct ANASTASIS_Policy *p; + + p = GNUNET_new (struct ANASTASIS_Policy); + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &p->salt, + sizeof (p->salt)); + { + struct ANASTASIS_CRYPTO_KeyShareP key_shares[truths_len]; + + for (unsigned int i = 0; i < truths_len; i++) + key_shares[i] = truths[i]->key_share; + ANASTASIS_CRYPTO_policy_key_derive (key_shares, + truths_len, + &p->salt, + &p->policy_key); + } + p->truths = GNUNET_new_array (truths_len, + struct ANASTASIS_Truth *); + for (unsigned int i = 0; i<truths_len; i++) + p->truths[i] = truth_dup (truths[i]); + p->truths_length = truths_len; + return p; +} + + +void +ANASTASIS_policy_destroy (struct ANASTASIS_Policy *p) +{ + for (unsigned int i = 0; i<p->truths_length; i++) + ANASTASIS_truth_free (p->truths[i]); + GNUNET_free (p->truths); + GNUNET_free (p); +} + + +/** + * State for a "policy store" CMD. + */ +struct PolicyStoreState +{ + /** + * User identifier used as entropy source for the account public key + */ + struct ANASTASIS_CRYPTO_UserIdentifierP id; + + /** + * Hash of the current upload. Used to check the server's response. + */ + struct GNUNET_HashCode curr_hash; + + /** + * Payment identifier. + */ + struct ANASTASIS_PaymentSecretP payment_secret; + + /** + * Server salt. Points into a truth object from which we got the + * salt. + */ + struct ANASTASIS_CRYPTO_ProviderSaltP server_salt; + + /** + * The /policy POST operation handle. + */ + struct ANASTASIS_PolicyStoreOperation *pso; + + /** + * URL of the anastasis backend. + */ + char *anastasis_url; + + /** + * Payment request returned by this provider, if any. + */ + char *payment_request; + + /** + * reference to SecretShare + */ + struct ANASTASIS_SecretShare *ss; + + /** + * Version of the policy created at the provider. + */ + unsigned long long policy_version; + + /** + * When will the policy expire at the provider. + */ + struct GNUNET_TIME_Absolute policy_expiration; + +}; + +/** +* Defines a recovery document upload process (recovery document consists of multiple policies) +*/ +struct ANASTASIS_SecretShare +{ + /** + * Closure for the Result Callback + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Callback which gives back the result of the POST Request + */ + ANASTASIS_ShareResultCallback src; + + /** + * Closure for the Result Callback + */ + void *src_cls; + + /** + * References for the upload states and operations (size of truths passed) + */ + struct PolicyStoreState *pss; + + /** + * Closure for the Result Callback + */ + unsigned int pss_length; +}; + + +/** + * Callback to process a POST /policy request + * + * @param cls closure + * @param ec anastasis-specific error code + * @param obj the decoded response body + */ +static void +policy_store_cb (void *cls, + const struct ANASTASIS_UploadDetails *ud) +{ + struct PolicyStoreState *pss = cls; + struct ANASTASIS_SecretShare *ss = pss->ss; + enum ANASTASIS_UploadStatus us; + + pss->pso = NULL; + if (NULL == ud) + us = ANASTASIS_US_HTTP_ERROR; + else + us = ud->us; + if ( (ANASTASIS_US_SUCCESS == us) && + (0 != GNUNET_memcmp (&pss->curr_hash, + ud->details.success.curr_backup_hash)) ) + { + GNUNET_break_op (0); + us = ANASTASIS_US_SERVER_ERROR; + } + switch (us) + { + case ANASTASIS_US_SUCCESS: + pss->policy_version = ud->details.success.policy_version; + pss->policy_expiration = ud->details.success.policy_expiration; + break; + case ANASTASIS_US_PAYMENT_REQUIRED: + pss->payment_request = GNUNET_strdup (ud->details.payment.payment_request); + pss->payment_secret = ud->details.payment.ps; + break; + case ANASTASIS_US_HTTP_ERROR: + case ANASTASIS_US_CLIENT_ERROR: + case ANASTASIS_US_SERVER_ERROR: + { + struct ANASTASIS_ShareResult sr = { + .ss = ANASTASIS_SHARE_STATUS_PROVIDER_FAILED, + .details.provider_failure.provider_url = pss->anastasis_url, + .details.provider_failure.http_status = ud->http_status, + .details.provider_failure.ec = us, + }; + + ss->src (ss->src_cls, + &sr); + ANASTASIS_secret_share_cancel (ss); + return; + } + case ANASTASIS_US_CONFLICTING_TRUTH: + GNUNET_break (0); + break; + } + for (unsigned int i = 0; i<ss->pss_length; i++) + if (NULL != ss->pss[i].pso) + /* some upload is still pending, let's wait for it to finish */ + return; + + { + struct ANASTASIS_SharePaymentRequest spr[GNUNET_NZL (ss->pss_length)]; + struct ANASTASIS_ProviderSuccessStatus apss[GNUNET_NZL (ss->pss_length)]; + unsigned int off = 0; + unsigned int voff = 0; + struct ANASTASIS_ShareResult sr; + + for (unsigned int i = 0; i<ss->pss_length; i++) + { + struct PolicyStoreState *pssi = &ss->pss[i]; + + if (NULL == pssi->payment_request) + { + apss[voff].policy_version = pssi->policy_version; + apss[voff].provider_url = pssi->anastasis_url; + apss[voff].policy_expiration = pssi->policy_expiration; + voff++; + } + else + { + spr[off].payment_request_url = pssi->payment_request; + spr[off].provider_url = pssi->anastasis_url; + spr[off].payment_secret = pssi->payment_secret; + off++; + } + } + if (off > 0) + { + sr.ss = ANASTASIS_SHARE_STATUS_PAYMENT_REQUIRED; + sr.details.payment_required.payment_requests = spr; + sr.details.payment_required.payment_requests_length = off; + } + else + { + sr.ss = ANASTASIS_SHARE_STATUS_SUCCESS; + sr.details.success.pss = apss; + sr.details.success.num_providers = voff; + } + ss->src (ss->src_cls, + &sr); + } + ANASTASIS_secret_share_cancel (ss); +} + + +struct ANASTASIS_SecretShare * +ANASTASIS_secret_share (struct GNUNET_CURL_Context *ctx, + const json_t *id_data, + const struct ANASTASIS_ProviderDetails providers[], + unsigned int pss_length, + const struct ANASTASIS_Policy *policies[], + unsigned int policies_len, + uint32_t payment_years_requested, + struct GNUNET_TIME_Relative pay_timeout, + ANASTASIS_ShareResultCallback src, + void *src_cls, + const char *secret_name, + const void *core_secret, + size_t core_secret_size) +{ + struct ANASTASIS_SecretShare *ss; + struct ANASTASIS_CRYPTO_EncryptedMasterKeyP + encrypted_master_keys[GNUNET_NZL (policies_len)]; + void *encrypted_core_secret; + json_t *dec_policies; + json_t *esc_methods; + size_t recovery_document_size; + char *recovery_document_str; + + if (0 == pss_length) + { + GNUNET_break (0); + return NULL; + } + ss = GNUNET_new (struct ANASTASIS_SecretShare); + ss->src = src; + ss->src_cls = src_cls; + ss->pss = GNUNET_new_array (pss_length, + struct PolicyStoreState); + ss->pss_length = pss_length; + ss->ctx = ctx; + + { + struct ANASTASIS_CRYPTO_PolicyKeyP policy_keys[GNUNET_NZL (policies_len)]; + + for (unsigned int i = 0; i < policies_len; i++) + policy_keys[i] = policies[i]->policy_key; + ANASTASIS_CRYPTO_core_secret_encrypt (policy_keys, + policies_len, + core_secret, + core_secret_size, + &encrypted_core_secret, + encrypted_master_keys); + } + dec_policies = json_array (); + GNUNET_assert (NULL != dec_policies); + for (unsigned int k = 0; k < policies_len; k++) + { + const struct ANASTASIS_Policy *policy = policies[k]; + json_t *uuids = json_array (); + + GNUNET_assert (NULL != uuids); + for (unsigned int b = 0; b < policy->truths_length; b++) + GNUNET_assert (0 == + json_array_append_new ( + uuids, + GNUNET_JSON_from_data_auto ( + &policy->truths[b]->uuid))); + if (0 != + json_array_append_new ( + dec_policies, + json_pack ("{s:o, s:o, s:o}", + "master_key", + GNUNET_JSON_from_data_auto ( + &encrypted_master_keys[k]), + "uuids", + uuids, + "salt", + GNUNET_JSON_from_data_auto (&policy->salt)))) + { + GNUNET_break (0); + json_decref (dec_policies); + ANASTASIS_secret_share_cancel (ss); + return NULL; + } + } + + esc_methods = json_array (); + for (unsigned int k = 0; k < policies_len; k++) + { + const struct ANASTASIS_Policy *policy = policies[k]; + + for (unsigned int l = 0; l < policy->truths_length; l++) + { + const struct ANASTASIS_Truth *pt = policy->truths[l]; + bool unique = true; + + /* Only append each truth once */ + for (unsigned int k2 = 0; k2 < k; k2++) + { + const struct ANASTASIS_Policy *p2 = policies[k2]; + for (unsigned int l2 = 0; l2 < p2->truths_length; l2++) + if (0 == + GNUNET_memcmp (&pt->uuid, + &p2->truths[l2]->uuid)) + { + unique = false; + break; + } + if (! unique) + break; + } + if (! unique) + continue; + + if (0 != + json_array_append_new ( + esc_methods, + json_pack ("{s:o," /* truth uuid */ + " s:s," /* provider url */ + " s:s," /* instructions */ + " s:o," /* truth key */ + " s:o," /* truth salt */ + " s:o," /* provider salt */ + " s:s}", /* escrow method */ + "uuid", + GNUNET_JSON_from_data_auto ( + &pt->uuid), + "url", + pt->url, + "instructions", + pt->instructions, + "truth_key", GNUNET_JSON_from_data_auto ( + &pt->truth_key), + "salt", GNUNET_JSON_from_data_auto ( + &pt->salt), + "provider_salt", GNUNET_JSON_from_data_auto ( + &pt->provider_salt), + "escrow_type", + pt->type))) + { + GNUNET_break (0); + json_decref (esc_methods); + json_decref (dec_policies); + ANASTASIS_secret_share_cancel (ss); + return NULL; + } + } + } + + { + json_t *recovery_document; + size_t rd_size; + char *rd_str; + Bytef *cbuf; + uLongf cbuf_size; + int ret; + uint32_t be_size; + + recovery_document = json_pack ( + "{s:s?, s:o, s:o, s:o}", + "secret_name", secret_name, + "policies", dec_policies, + "escrow_methods", esc_methods, + "encrypted_core_secret", GNUNET_JSON_from_data (encrypted_core_secret, + core_secret_size)); + GNUNET_assert (NULL != recovery_document); + GNUNET_free (encrypted_core_secret); + + rd_str = json_dumps (recovery_document, + JSON_COMPACT | JSON_SORT_KEYS); + GNUNET_assert (NULL != rd_str); + json_decref (recovery_document); + rd_size = strlen (rd_str); + cbuf_size = compressBound (rd_size); + be_size = htonl ((uint32_t) rd_size); + cbuf = GNUNET_malloc (cbuf_size + sizeof (uint32_t)); + memcpy (cbuf, + &be_size, + sizeof (uint32_t)); + ret = compress (cbuf + sizeof (uint32_t), + &cbuf_size, + (const Bytef *) rd_str, + rd_size); + if (Z_OK != ret) + { + /* compression failed!? */ + GNUNET_break (0); + free (rd_str); + GNUNET_free (cbuf); + ANASTASIS_secret_share_cancel (ss); + return NULL; + } + free (rd_str); + recovery_document_size = (size_t) (cbuf_size + sizeof (uint32_t)); + recovery_document_str = (char *) cbuf; + } + + for (unsigned int l = 0; l < ss->pss_length; l++) + { + struct PolicyStoreState *pss = &ss->pss[l]; + void *recovery_data; + size_t recovery_data_size; + struct ANASTASIS_CRYPTO_AccountPrivateKeyP anastasis_priv; + + pss->ss = ss; + pss->anastasis_url = GNUNET_strdup (providers[l].provider_url); + pss->server_salt = providers[l].provider_salt; + pss->payment_secret = providers[l].payment_secret; + ANASTASIS_CRYPTO_user_identifier_derive (id_data, + &pss->server_salt, + &pss->id); + ANASTASIS_CRYPTO_account_private_key_derive (&pss->id, + &anastasis_priv); + ANASTASIS_CRYPTO_recovery_document_encrypt (&pss->id, + recovery_document_str, + recovery_document_size, + &recovery_data, + &recovery_data_size); + GNUNET_CRYPTO_hash (recovery_data, + recovery_data_size, + &pss->curr_hash); + pss->pso = ANASTASIS_policy_store ( + ss->ctx, + pss->anastasis_url, + &anastasis_priv, + recovery_data, + recovery_data_size, + payment_years_requested, + (! GNUNET_is_zero (&pss->payment_secret)) + ? &pss->payment_secret + : NULL, + pay_timeout, + &policy_store_cb, + pss); + GNUNET_free (recovery_data); + if (NULL == pss->pso) + { + GNUNET_break (0); + ANASTASIS_secret_share_cancel (ss); + GNUNET_free (recovery_document_str); + return NULL; + } + } + GNUNET_free (recovery_document_str); + return ss; +} + + +void +ANASTASIS_secret_share_cancel (struct ANASTASIS_SecretShare *ss) +{ + for (unsigned int i = 0; i<ss->pss_length; i++) + { + struct PolicyStoreState *pssi = &ss->pss[i]; + + if (NULL != pssi->pso) + { + ANASTASIS_policy_store_cancel (pssi->pso); + pssi->pso = NULL; + } + GNUNET_free (pssi->anastasis_url); + GNUNET_free (pssi->payment_request); + } + GNUNET_free (ss->pss); + GNUNET_free (ss); +} diff --git a/src/lib/anastasis_recovery.c b/src/lib/anastasis_recovery.c new file mode 100644 index 0000000..5b0726f --- /dev/null +++ b/src/lib/anastasis_recovery.c @@ -0,0 +1,1425 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 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 <http://www.gnu.org/licenses/> +*/ +/** + * @brief anastasis client api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis.h" +#include <taler/taler_json_lib.h> +#include <gnunet/gnunet_util_lib.h> +#include <taler/taler_merchant_service.h> +#include <zlib.h> + + +/** + * Challenge struct contains the uuid and public key's needed for the + * recovery process and a reference to ANASTASIS_Recovery. + */ +struct ANASTASIS_Challenge +{ + + /** + * Information exported to clients about this challenge. + */ + struct ANASTASIS_ChallengeDetails ci; + + /** + * Key used to encrypt the truth passed to the server + */ + struct ANASTASIS_CRYPTO_TruthKeyP truth_key; + + /** + * Salt; used to derive hash from security question answers. + */ + struct ANASTASIS_CRYPTO_QuestionSaltP salt; + + /** + * Provider salt; used to derive our key material from our identity + * key. + */ + struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt; + + /** + * Decrypted key share for this challenge. Set once the + * challenge was @e ri.solved. + */ + struct ANASTASIS_CRYPTO_KeyShareP key_share; + + /** + * Callback which gives back the instructions and a status code of + * the request to the user when answering a challenge was initiated. + */ + ANASTASIS_AnswerFeedback af; + + /** + * Closure for the challenge callback + */ + void *af_cls; + + /** + * Defines the base URL of the Anastasis provider used for the challenge. + */ + char *url; + + /** + * What is the type of this challenge (E-Mail, Security Question, SMS...) + */ + char *type; + + /** + * Instructions for solving the challenge (generic, set client-side + * when challenge was established). + */ + char *instructions; + + /** + * Answer to the security question, if @a type is "question". Otherwise NULL. + */ + char *answer; + + /** + * Reference to the recovery process which is ongoing + */ + struct ANASTASIS_Recovery *recovery; + + /** + * keyshare lookup operation + */ + struct ANASTASIS_KeyShareLookupOperation *kslo; + +}; + + +/** + * Defines a decryption policy with multiple escrow methods + */ +struct DecryptionPolicy +{ + + /** + * Publicly visible details about a decryption policy. + */ + struct ANASTASIS_DecryptionPolicy pub_details; + + /** + * Encrypted masterkey (encrypted with the policy key). + */ + struct ANASTASIS_CRYPTO_EncryptedMasterKeyP emk; + + /** + * Salt used to decrypt master key. + */ + struct ANASTASIS_CRYPTO_MasterSaltP salt; + +}; + + +/** + * stores provider URLs, identity key material, decrypted recovery document (internally!) + */ +struct ANASTASIS_Recovery +{ + + /** + * Identity key material used for the derivation of keys + */ + struct ANASTASIS_CRYPTO_UserIdentifierP id; + + /** + * Recovery information which is given to the user + */ + struct ANASTASIS_RecoveryInformation ri; + + /** + * Internal of @e ri.dps_len policies that would allow recovery of the core secret. + */ + struct DecryptionPolicy *dps; + + /** + * Array of @e ri.cs_len challenges to be solved (for any of the policies). + */ + struct ANASTASIS_Challenge *cs; + + /** + * Identity data to user id from. + */ + json_t *id_data; + + /** + * Callback to send back a recovery document with the policies and the version + */ + ANASTASIS_PolicyCallback pc; + + /** + * closure for the Policy callback + */ + void *pc_cls; + + /** + * Callback to send back the core secret which was saved by + * anastasis, after all challenges are completed + */ + ANASTASIS_CoreSecretCallback csc; + + /** + * Closure for the core secret callback + */ + void *csc_cls; + + /** + * Curl context + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Reference to the policy lookup operation which is executed + */ + struct ANASTASIS_PolicyLookupOperation *plo; + + /** + * Array of challenges that have been solved. + * Valid entries up to @e solved_challenge_pos. + * Length matches the total number of challenges in @e ri. + */ + struct ANASTASIS_Challenge **solved_challenges; + + /** + * Our provider URL. + */ + char *provider_url; + + /** + * Name of the secret, can be NULL. + */ + char *secret_name; + + /** + * Task to run @e pc asynchronously. + */ + struct GNUNET_SCHEDULER_Task *do_async; + + /** + * Retrieved encrypted core secret from policy + */ + void *enc_core_secret; + + /** + * Size of the @e enc_core_secret + */ + size_t enc_core_secret_size; + + /** + * Current offset in the @e solved_challenges array. + */ + unsigned int solved_challenge_pos; + +}; + + +/** + * Function called with the results of a #ANASTASIS_keyshare_lookup(). + * + * @param cls closure + * @param http_status HTTP status of the request + * @param ud details about the lookup operation + */ +static void +keyshare_lookup_cb (void *cls, + const struct ANASTASIS_KeyShareDownloadDetails *dd) +{ + struct ANASTASIS_Challenge *c = cls; + struct ANASTASIS_Recovery *recovery = c->recovery; + struct ANASTASIS_CRYPTO_UserIdentifierP id; + struct DecryptionPolicy *rdps; + + c->kslo = NULL; + switch (dd->status) + { + case ANASTASIS_KSD_SUCCESS: + break; + case ANASTASIS_KSD_PAYMENT_REQUIRED: + { + struct ANASTASIS_ChallengeStartResponse csr = { + .cs = ANASTASIS_CHALLENGE_STATUS_PAYMENT_REQUIRED, + .challenge = c, + .details.payment_required.taler_pay_uri + = dd->details.payment_required.taler_pay_uri, + .details.payment_required.payment_secret + = dd->details.payment_required.payment_secret + }; + + c->af (c->af_cls, + &csr); + return; + } + case ANASTASIS_KSD_INVALID_ANSWER: + { + struct ANASTASIS_ChallengeStartResponse csr = { + .cs = ANASTASIS_CHALLENGE_STATUS_INSTRUCTIONS, + .challenge = c, + .details.open_challenge.body + = dd->details.open_challenge.body, + .details.open_challenge.content_type + = dd->details.open_challenge.content_type, + .details.open_challenge.body_size + = dd->details.open_challenge.body_size, + .details.open_challenge.http_status + = dd->details.open_challenge.http_status + }; + + c->af (c->af_cls, + &csr); + return; + } + case ANASTASIS_KSD_REDIRECT_FOR_AUTHENTICATION: + { + struct ANASTASIS_ChallengeStartResponse csr = { + .cs = ANASTASIS_CHALLENGE_STATUS_REDIRECT_FOR_AUTHENTICATION, + .challenge = c, + .details.redirect_url + = dd->details.redirect_url + }; + + c->af (c->af_cls, + &csr); + return; + } + case ANASTASIS_KSD_TRUTH_UNKNOWN: + { + struct ANASTASIS_ChallengeStartResponse csr = { + .cs = ANASTASIS_CHALLENGE_STATUS_TRUTH_UNKNOWN, + .challenge = c + }; + + c->af (c->af_cls, + &csr); + return; + } + case ANASTASIS_KSD_RATE_LIMIT_EXCEEDED: + { + struct ANASTASIS_ChallengeStartResponse csr = { + .cs = ANASTASIS_CHALLENGE_STATUS_RATE_LIMIT_EXCEEDED, + .challenge = c + }; + + c->af (c->af_cls, + &csr); + return; + } + case ANASTASIS_KSD_SERVER_ERROR: + case ANASTASIS_KSD_CLIENT_FAILURE: + { + struct ANASTASIS_ChallengeStartResponse csr = { + .cs = ANASTASIS_CHALLENGE_STATUS_SERVER_FAILURE, + .challenge = c, + .details.server_failure.ec + = dd->details.server_failure.ec, + .details.server_failure.http_status + = dd->details.server_failure.http_status + }; + + c->af (c->af_cls, + &csr); + return; + } + } + + GNUNET_assert (NULL != dd); + ANASTASIS_CRYPTO_user_identifier_derive (recovery->id_data, + &c->provider_salt, + &id); + ANASTASIS_CRYPTO_keyshare_decrypt (&dd->details.eks, + &id, + c->answer, + &c->key_share); + recovery->solved_challenges[recovery->solved_challenge_pos++] = c; + + { + struct ANASTASIS_ChallengeStartResponse csr = { + .cs = ANASTASIS_CHALLENGE_STATUS_SOLVED, + .challenge = c + }; + + c->ci.solved = true; + c->af (c->af_cls, + &csr); + } + + + /* Check if there is a policy for which all challenges have + been satisfied, if so, store it in 'rdps'. */ + rdps = NULL; + for (unsigned int i = 0; i < recovery->ri.dps_len; i++) + { + struct DecryptionPolicy *dps = &recovery->dps[i]; + bool missing = false; + + for (unsigned int j = 0; j < dps->pub_details.challenges_length; j++) + { + bool found = false; + + for (unsigned int k = 0; k < recovery->solved_challenge_pos; k++) + { + if (dps->pub_details.challenges[j] == recovery->solved_challenges[k]) + { + found = true; + break; + } + } + if (! found) + { + missing = true; + break; + } + } + if (! missing) + { + rdps = dps; + break; + } + } + if (NULL == rdps) + return; + + { + void *core_secret; + size_t core_secret_size; + struct ANASTASIS_CRYPTO_KeyShareP + key_shares[rdps->pub_details.challenges_length]; + struct ANASTASIS_CRYPTO_PolicyKeyP policy_key; + + for (unsigned int l = 0; l < rdps->pub_details.challenges_length; l++) + for (unsigned int m = 0; m < recovery->solved_challenge_pos; m++) + if (rdps->pub_details.challenges[l] == recovery->solved_challenges[m]) + key_shares[l] = recovery->solved_challenges[m]->key_share; + ANASTASIS_CRYPTO_policy_key_derive (key_shares, + rdps->pub_details.challenges_length, + &rdps->salt, + &policy_key); + ANASTASIS_CRYPTO_core_secret_recover (&rdps->emk, + &policy_key, + recovery->enc_core_secret, + recovery->enc_core_secret_size, + &core_secret, + &core_secret_size); + recovery->csc (recovery->csc_cls, + ANASTASIS_RS_SUCCESS, + core_secret, + core_secret_size); + GNUNET_free (core_secret); + ANASTASIS_recovery_abort (recovery); + } +} + + +const struct ANASTASIS_ChallengeDetails * +ANASTASIS_challenge_get_details (struct ANASTASIS_Challenge *challenge) +{ + return &challenge->ci; +} + + +int +ANASTASIS_challenge_start (struct ANASTASIS_Challenge *c, + const struct ANASTASIS_PaymentSecretP *psp, + struct GNUNET_TIME_Relative timeout, + const struct GNUNET_HashCode *hashed_answer, + ANASTASIS_AnswerFeedback af, + void *af_cls) +{ + if (c->ci.solved) + { + GNUNET_break (0); + return GNUNET_NO; /* already solved */ + } + if (NULL != c->kslo) + { + GNUNET_break (0); + return GNUNET_NO; /* already solving */ + } + c->af = af; + c->af_cls = af_cls; + c->kslo = ANASTASIS_keyshare_lookup (c->recovery->ctx, + c->url, + &c->ci.uuid, + &c->truth_key, + psp, + timeout, + hashed_answer, + &keyshare_lookup_cb, + c); + if (NULL == c->kslo) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +int +ANASTASIS_challenge_answer ( + struct ANASTASIS_Challenge *c, + const struct ANASTASIS_PaymentSecretP *psp, + struct GNUNET_TIME_Relative timeout, + const char *answer_str, + ANASTASIS_AnswerFeedback af, + void *af_cls) +{ + struct GNUNET_HashCode hashed_answer; + + GNUNET_free (c->answer); + c->answer = GNUNET_strdup (answer_str); + ANASTASIS_CRYPTO_secure_answer_hash (answer_str, + &c->ci.uuid, + &c->salt, + &hashed_answer); + return ANASTASIS_challenge_start (c, + psp, + timeout, + &hashed_answer, + af, + af_cls); +} + + +int +ANASTASIS_challenge_answer2 (struct ANASTASIS_Challenge *c, + const struct ANASTASIS_PaymentSecretP *psp, + struct GNUNET_TIME_Relative timeout, + uint64_t answer, + ANASTASIS_AnswerFeedback af, + void *af_cls) +{ + struct GNUNET_HashCode answer_s; + + ANASTASIS_hash_answer (answer, + &answer_s); + return ANASTASIS_challenge_start (c, + psp, + timeout, + &answer_s, + af, + af_cls); +} + + +void +ANASTASIS_challenge_abort (struct ANASTASIS_Challenge *c) +{ + if (NULL == c->kslo) + { + GNUNET_break (0); + return; + } + ANASTASIS_keyshare_lookup_cancel (c->kslo); + c->kslo = NULL; + c->af = NULL; + c->af_cls = NULL; +} + + +/** + * Function called with the results of a ANASTASIS_policy_lookup + * + * @param cls closure + * @param http_status HTTP status of the request + * @param ud details about the lookup operation + */ +static void +policy_lookup_cb (void *cls, + unsigned int http_status, + const struct ANASTASIS_DownloadDetails *dd) +{ + struct ANASTASIS_Recovery *r = cls; + void *plaintext; + size_t size_plaintext; + json_error_t json_error; + json_t *dec_policies; + json_t *esc_methods; + + r->plo = NULL; + switch (http_status) + { + case MHD_HTTP_OK: + break; + case MHD_HTTP_NOT_FOUND: + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_UNKNOWN, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + case MHD_HTTP_NO_CONTENT: + /* Account known, policy expired */ + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_GONE, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Bad server... */ + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_SERVER_ERROR, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + case MHD_HTTP_NOT_MODIFIED: + /* Should not be possible, we do not cache, fall-through! */ + default: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u in %s:%u\n", + http_status, + __FILE__, + __LINE__); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_DOWNLOAD_FAILED, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + } + if (NULL == dd->policy) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No recovery data available"); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_DOWNLOAD_NO_POLICY, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + } + ANASTASIS_CRYPTO_recovery_document_decrypt (&r->id, + dd->policy, + dd->policy_size, + &plaintext, + &size_plaintext); + if (size_plaintext < sizeof (uint32_t)) + { + GNUNET_break_op (0); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_DOWNLOAD_INVALID_COMPRESSION, + NULL, + 0); + ANASTASIS_recovery_abort (r); + GNUNET_free (plaintext); + return; + } + { + json_t *recovery_document; + uint32_t be_size; + uLongf pt_size; + char *pt; + + memcpy (&be_size, + plaintext, + sizeof (uint32_t)); + pt_size = ntohl (be_size); + pt = GNUNET_malloc_large (pt_size); + if (NULL == pt) + { + GNUNET_break_op (0); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_DOWNLOAD_TOO_BIG, + NULL, + 0); + ANASTASIS_recovery_abort (r); + GNUNET_free (plaintext); + return; + } + if (Z_OK != + uncompress ((Bytef *) pt, + &pt_size, + (const Bytef *) plaintext + sizeof (uint32_t), + size_plaintext - sizeof (uint32_t))) + { + GNUNET_break_op (0); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_DOWNLOAD_INVALID_COMPRESSION, + NULL, + 0); + GNUNET_free (plaintext); + GNUNET_free (pt); + ANASTASIS_recovery_abort (r); + return; + } + GNUNET_free (plaintext); + recovery_document = json_loadb ((char *) pt, + pt_size, + JSON_DECODE_ANY, + &json_error); + GNUNET_free (pt); + if (NULL == recovery_document) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to read JSON input: %s at %d:%s (offset: %d)\n", + json_error.text, + json_error.line, + |