From 7e669bcf6b6336ec429da949bcb4aa456971dba2 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Fri, 30 Jul 2021 10:38:27 +0200 Subject: folding history in preparation of GNU Anastasis v0.0.0 release --- src/Makefile.am | 3 + src/authorization/Makefile.am | 98 + src/authorization/anastasis_authorization_plugin.c | 239 + .../anastasis_authorization_plugin_email.c | 616 +++ .../anastasis_authorization_plugin_file.c | 302 ++ .../anastasis_authorization_plugin_post.c | 655 +++ .../anastasis_authorization_plugin_sms.c | 607 +++ .../authorization-email-messages.json | 10 + src/authorization/authorization-post-messages.json | 7 + src/authorization/authorization-sms-messages.json | 6 + src/backend/Makefile.am | 45 + src/backend/anastasis-httpd.c | 943 ++++ src/backend/anastasis-httpd.h | 225 + src/backend/anastasis-httpd_config.c | 132 + src/backend/anastasis-httpd_config.h | 41 + src/backend/anastasis-httpd_mhd.c | 70 + src/backend/anastasis-httpd_mhd.h | 61 + src/backend/anastasis-httpd_policy.c | 252 + src/backend/anastasis-httpd_policy.h | 66 + src/backend/anastasis-httpd_policy_upload.c | 1211 +++++ src/backend/anastasis-httpd_terms.c | 98 + src/backend/anastasis-httpd_terms.h | 62 + src/backend/anastasis-httpd_truth.c | 1428 ++++++ src/backend/anastasis-httpd_truth.h | 75 + src/backend/anastasis-httpd_truth_upload.c | 855 ++++ src/backend/anastasis.conf | 77 + src/cli/.gitignore | 6 + src/cli/Makefile.am | 56 + src/cli/anastasis-cli-redux.c | 366 ++ src/cli/resources/00-backup.json | 8 + src/cli/resources/00-recovery.json | 8 + src/cli/resources/01-backup.json | 41 + src/cli/resources/01-recovery.json | 41 + src/cli/resources/02-backup.json | 83 + src/cli/resources/02-recovery.json | 83 + src/cli/resources/03-backup.json | 155 + src/cli/resources/04-backup.json | 172 + src/cli/resources/05-backup.json | 213 + src/cli/resources/06-backup.json | 223 + src/cli/test_anastasis_reducer_1.conf | 9 + src/cli/test_anastasis_reducer_2.conf | 9 + src/cli/test_anastasis_reducer_3.conf | 9 + src/cli/test_anastasis_reducer_4.conf | 9 + .../test_anastasis_reducer_add_authentication.sh | 134 + ...astasis_reducer_backup_enter_user_attributes.sh | 140 + .../test_anastasis_reducer_done_authentication.sh | 65 + .../test_anastasis_reducer_done_policy_review.sh | 105 + src/cli/test_anastasis_reducer_enter_secret.sh | 417 ++ src/cli/test_anastasis_reducer_initialize_state.sh | 64 + ...tasis_reducer_recovery_enter_user_attributes.sh | 516 +++ src/cli/test_anastasis_reducer_select_continent.sh | 116 + src/cli/test_anastasis_reducer_select_country.sh | 144 + src/cli/test_reducer.conf | 197 + src/cli/user-details-example.json | 6 + src/include/Makefile.am | 15 + src/include/anastasis.h | 986 ++++ src/include/anastasis_authorization_lib.h | 52 + src/include/anastasis_authorization_plugin.h | 183 + src/include/anastasis_crypto_lib.h | 533 +++ src/include/anastasis_database_lib.h | 49 + src/include/anastasis_database_plugin.h | 655 +++ src/include/anastasis_json.h | 410 ++ src/include/anastasis_redux.h | 127 + src/include/anastasis_service.h | 703 +++ src/include/anastasis_testing_lib.h | 884 ++++ src/include/anastasis_util_lib.h | 82 + src/include/gettext.h | 71 + src/include/platform.h | 60 + src/lib/Makefile.am | 27 + src/lib/anastasis_backup.c | 979 ++++ src/lib/anastasis_recovery.c | 1425 ++++++ src/lib/test_merchant.priv | 1 + src/reducer/Makefile.am | 45 + src/reducer/anastasis_api_backup_redux.c | 4893 ++++++++++++++++++++ src/reducer/anastasis_api_recovery_redux.c | 2558 ++++++++++ src/reducer/anastasis_api_redux.c | 1730 +++++++ src/reducer/anastasis_api_redux.h | 347 ++ src/reducer/validation_CH_AHV.c | 57 + src/reducer/validation_CZ_BN.c | 59 + src/reducer/validation_DE_SVN.c | 98 + src/reducer/validation_DE_TIN.c | 57 + src/reducer/validation_IN_AADHAR.c | 113 + src/reducer/validation_IT_CF.c | 198 + src/reducer/validation_XX_SQUARE.c | 48 + src/reducer/validation_XY_PRIME.c | 53 + src/restclient/Makefile.am | 42 + src/restclient/anastasis_api_config.c | 317 ++ src/restclient/anastasis_api_curl_defaults.c | 46 + src/restclient/anastasis_api_curl_defaults.h | 38 + src/restclient/anastasis_api_keyshare_lookup.c | 508 ++ src/restclient/anastasis_api_policy_lookup.c | 356 ++ src/restclient/anastasis_api_policy_store.c | 527 +++ src/restclient/anastasis_api_truth_store.c | 354 ++ src/stasis/Datenbank-Schema.xml | 1 + src/stasis/Makefile.am | 95 + src/stasis/anastasis-dbinit.c | 112 + src/stasis/anastasis_db_plugin.c | 146 + src/stasis/anastasis_db_postgres.conf | 7 + src/stasis/drop0001.sql | 37 + src/stasis/plugin_anastasis_postgres.c | 2301 +++++++++ src/stasis/stasis-0000.sql | 293 ++ src/stasis/stasis-0001.sql | 194 + src/stasis/stasis-postgres.conf | 6 + src/stasis/test_anastasis_db.c | 344 ++ src/stasis/test_anastasis_db_postgres.conf | 10 + src/testing/.gitignore | 3 + src/testing/Makefile.am | 91 + src/testing/sms_authentication.sh | 10 + src/testing/test_anastasis.c | 464 ++ src/testing/test_anastasis_api.c | 421 ++ src/testing/test_anastasis_api.conf | 264 ++ .../.config/taler/exchange/account-2.json | 3 + .../.config/taler/merchant/account-3.json | 1 + .../.config/taler/merchant/default.priv | 1 + .../.config/taler/merchant/dtip.priv | 1 + .../.config/taler/merchant/nulltip.priv | 1 + .../.config/taler/merchant/reserve/dtip.priv | 1 + .../.config/taler/merchant/reserve/nulltip.priv | 1 + .../.config/taler/merchant/reserve/tip.priv | 1 + .../.config/taler/merchant/tip.priv | Bin 0 -> 32 bytes .../.config/taler/merchant/tor.priv | Bin 0 -> 32 bytes .../.config/taler/test.json | 8 + .../share/taler/exchange/offline-keys/master.priv | 1 + .../.local/share/taler/merchant/merchant.priv | 1 + src/testing/testing_api_cmd_config.c | 206 + src/testing/testing_api_cmd_keyshare_lookup.c | 465 ++ src/testing/testing_api_cmd_policy_lookup.c | 267 ++ src/testing/testing_api_cmd_policy_store.c | 397 ++ src/testing/testing_api_cmd_truth_store.c | 436 ++ src/testing/testing_api_helpers.c | 173 + src/testing/testing_api_trait_account_priv.c | 72 + src/testing/testing_api_trait_account_pub.c | 72 + src/testing/testing_api_trait_code.c | 73 + src/testing/testing_api_trait_eks.c | 58 + src/testing/testing_api_trait_hash.c | 72 + src/testing/testing_api_trait_payment_secret.c | 73 + src/testing/testing_api_trait_salt.c | 74 + src/testing/testing_api_trait_truth_key.c | 58 + src/testing/testing_api_trait_truth_uuid.c | 74 + src/testing/testing_cmd_challenge_answer.c | 584 +++ src/testing/testing_cmd_policy_create.c | 208 + src/testing/testing_cmd_recover_secret.c | 518 +++ src/testing/testing_cmd_secret_share.c | 441 ++ src/testing/testing_cmd_truth_upload.c | 383 ++ src/testing/testing_trait_challenge.c | 72 + src/testing/testing_trait_core_secret.c | 74 + src/testing/testing_trait_policy.c | 73 + src/testing/testing_trait_truth.c | 73 + src/util/Makefile.am | 57 + src/util/anastasis-config.in | 12 + src/util/anastasis_crypto.c | 637 +++ src/util/os_installation.c | 71 + src/util/paths.conf | 29 + src/util/test_anastasis_crypto.c | 346 ++ 154 files changed, 42232 insertions(+) create mode 100644 src/Makefile.am create mode 100644 src/authorization/Makefile.am create mode 100644 src/authorization/anastasis_authorization_plugin.c create mode 100644 src/authorization/anastasis_authorization_plugin_email.c create mode 100644 src/authorization/anastasis_authorization_plugin_file.c create mode 100644 src/authorization/anastasis_authorization_plugin_post.c create mode 100644 src/authorization/anastasis_authorization_plugin_sms.c create mode 100644 src/authorization/authorization-email-messages.json create mode 100644 src/authorization/authorization-post-messages.json create mode 100644 src/authorization/authorization-sms-messages.json create mode 100644 src/backend/Makefile.am create mode 100644 src/backend/anastasis-httpd.c create mode 100644 src/backend/anastasis-httpd.h create mode 100644 src/backend/anastasis-httpd_config.c create mode 100644 src/backend/anastasis-httpd_config.h create mode 100644 src/backend/anastasis-httpd_mhd.c create mode 100644 src/backend/anastasis-httpd_mhd.h create mode 100644 src/backend/anastasis-httpd_policy.c create mode 100644 src/backend/anastasis-httpd_policy.h create mode 100644 src/backend/anastasis-httpd_policy_upload.c create mode 100644 src/backend/anastasis-httpd_terms.c create mode 100644 src/backend/anastasis-httpd_terms.h create mode 100644 src/backend/anastasis-httpd_truth.c create mode 100644 src/backend/anastasis-httpd_truth.h create mode 100644 src/backend/anastasis-httpd_truth_upload.c create mode 100644 src/backend/anastasis.conf create mode 100644 src/cli/.gitignore create mode 100644 src/cli/Makefile.am create mode 100644 src/cli/anastasis-cli-redux.c create mode 100644 src/cli/resources/00-backup.json create mode 100644 src/cli/resources/00-recovery.json create mode 100644 src/cli/resources/01-backup.json create mode 100644 src/cli/resources/01-recovery.json create mode 100644 src/cli/resources/02-backup.json create mode 100644 src/cli/resources/02-recovery.json create mode 100644 src/cli/resources/03-backup.json create mode 100644 src/cli/resources/04-backup.json create mode 100644 src/cli/resources/05-backup.json create mode 100644 src/cli/resources/06-backup.json create mode 100644 src/cli/test_anastasis_reducer_1.conf create mode 100644 src/cli/test_anastasis_reducer_2.conf create mode 100644 src/cli/test_anastasis_reducer_3.conf create mode 100644 src/cli/test_anastasis_reducer_4.conf create mode 100755 src/cli/test_anastasis_reducer_add_authentication.sh create mode 100755 src/cli/test_anastasis_reducer_backup_enter_user_attributes.sh create mode 100755 src/cli/test_anastasis_reducer_done_authentication.sh create mode 100755 src/cli/test_anastasis_reducer_done_policy_review.sh create mode 100755 src/cli/test_anastasis_reducer_enter_secret.sh create mode 100755 src/cli/test_anastasis_reducer_initialize_state.sh create mode 100755 src/cli/test_anastasis_reducer_recovery_enter_user_attributes.sh create mode 100755 src/cli/test_anastasis_reducer_select_continent.sh create mode 100755 src/cli/test_anastasis_reducer_select_country.sh create mode 100644 src/cli/test_reducer.conf create mode 100644 src/cli/user-details-example.json create mode 100644 src/include/Makefile.am create mode 100644 src/include/anastasis.h create mode 100644 src/include/anastasis_authorization_lib.h create mode 100644 src/include/anastasis_authorization_plugin.h create mode 100644 src/include/anastasis_crypto_lib.h create mode 100644 src/include/anastasis_database_lib.h create mode 100644 src/include/anastasis_database_plugin.h create mode 100644 src/include/anastasis_json.h create mode 100644 src/include/anastasis_redux.h create mode 100644 src/include/anastasis_service.h create mode 100644 src/include/anastasis_testing_lib.h create mode 100644 src/include/anastasis_util_lib.h create mode 100644 src/include/gettext.h create mode 100644 src/include/platform.h create mode 100644 src/lib/Makefile.am create mode 100644 src/lib/anastasis_backup.c create mode 100644 src/lib/anastasis_recovery.c create mode 100644 src/lib/test_merchant.priv create mode 100644 src/reducer/Makefile.am create mode 100644 src/reducer/anastasis_api_backup_redux.c create mode 100644 src/reducer/anastasis_api_recovery_redux.c create mode 100644 src/reducer/anastasis_api_redux.c create mode 100644 src/reducer/anastasis_api_redux.h create mode 100644 src/reducer/validation_CH_AHV.c create mode 100644 src/reducer/validation_CZ_BN.c create mode 100644 src/reducer/validation_DE_SVN.c create mode 100644 src/reducer/validation_DE_TIN.c create mode 100644 src/reducer/validation_IN_AADHAR.c create mode 100644 src/reducer/validation_IT_CF.c create mode 100644 src/reducer/validation_XX_SQUARE.c create mode 100644 src/reducer/validation_XY_PRIME.c create mode 100644 src/restclient/Makefile.am create mode 100644 src/restclient/anastasis_api_config.c create mode 100644 src/restclient/anastasis_api_curl_defaults.c create mode 100644 src/restclient/anastasis_api_curl_defaults.h create mode 100644 src/restclient/anastasis_api_keyshare_lookup.c create mode 100644 src/restclient/anastasis_api_policy_lookup.c create mode 100644 src/restclient/anastasis_api_policy_store.c create mode 100644 src/restclient/anastasis_api_truth_store.c create mode 100644 src/stasis/Datenbank-Schema.xml create mode 100644 src/stasis/Makefile.am create mode 100644 src/stasis/anastasis-dbinit.c create mode 100644 src/stasis/anastasis_db_plugin.c create mode 100644 src/stasis/anastasis_db_postgres.conf create mode 100644 src/stasis/drop0001.sql create mode 100644 src/stasis/plugin_anastasis_postgres.c create mode 100644 src/stasis/stasis-0000.sql create mode 100644 src/stasis/stasis-0001.sql create mode 100644 src/stasis/stasis-postgres.conf create mode 100644 src/stasis/test_anastasis_db.c create mode 100644 src/stasis/test_anastasis_db_postgres.conf create mode 100644 src/testing/.gitignore create mode 100644 src/testing/Makefile.am create mode 100755 src/testing/sms_authentication.sh create mode 100644 src/testing/test_anastasis.c create mode 100644 src/testing/test_anastasis_api.c create mode 100644 src/testing/test_anastasis_api.conf create mode 100644 src/testing/test_anastasis_api_home/.config/taler/exchange/account-2.json create mode 100644 src/testing/test_anastasis_api_home/.config/taler/merchant/account-3.json create mode 100644 src/testing/test_anastasis_api_home/.config/taler/merchant/default.priv create mode 100644 src/testing/test_anastasis_api_home/.config/taler/merchant/dtip.priv create mode 100644 src/testing/test_anastasis_api_home/.config/taler/merchant/nulltip.priv create mode 100644 src/testing/test_anastasis_api_home/.config/taler/merchant/reserve/dtip.priv create mode 100644 src/testing/test_anastasis_api_home/.config/taler/merchant/reserve/nulltip.priv create mode 100644 src/testing/test_anastasis_api_home/.config/taler/merchant/reserve/tip.priv create mode 100644 src/testing/test_anastasis_api_home/.config/taler/merchant/tip.priv create mode 100644 src/testing/test_anastasis_api_home/.config/taler/merchant/tor.priv create mode 100644 src/testing/test_anastasis_api_home/.config/taler/test.json create mode 100644 src/testing/test_anastasis_api_home/.local/share/taler/exchange/offline-keys/master.priv create mode 100644 src/testing/test_anastasis_api_home/.local/share/taler/merchant/merchant.priv create mode 100644 src/testing/testing_api_cmd_config.c create mode 100644 src/testing/testing_api_cmd_keyshare_lookup.c create mode 100644 src/testing/testing_api_cmd_policy_lookup.c create mode 100644 src/testing/testing_api_cmd_policy_store.c create mode 100644 src/testing/testing_api_cmd_truth_store.c create mode 100644 src/testing/testing_api_helpers.c create mode 100644 src/testing/testing_api_trait_account_priv.c create mode 100644 src/testing/testing_api_trait_account_pub.c create mode 100644 src/testing/testing_api_trait_code.c create mode 100644 src/testing/testing_api_trait_eks.c create mode 100644 src/testing/testing_api_trait_hash.c create mode 100644 src/testing/testing_api_trait_payment_secret.c create mode 100644 src/testing/testing_api_trait_salt.c create mode 100644 src/testing/testing_api_trait_truth_key.c create mode 100644 src/testing/testing_api_trait_truth_uuid.c create mode 100644 src/testing/testing_cmd_challenge_answer.c create mode 100644 src/testing/testing_cmd_policy_create.c create mode 100644 src/testing/testing_cmd_recover_secret.c create mode 100644 src/testing/testing_cmd_secret_share.c create mode 100644 src/testing/testing_cmd_truth_upload.c create mode 100644 src/testing/testing_trait_challenge.c create mode 100644 src/testing/testing_trait_core_secret.c create mode 100644 src/testing/testing_trait_policy.c create mode 100644 src/testing/testing_trait_truth.c create mode 100644 src/util/Makefile.am create mode 100644 src/util/anastasis-config.in create mode 100644 src/util/anastasis_crypto.c create mode 100644 src/util/os_installation.c create mode 100644 src/util/paths.conf create mode 100644 src/util/test_anastasis_crypto.c (limited to 'src') 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 +*/ +/** + * @file anastasis_authorization_plugin.c + * @brief Logic to load database plugin + * @author Christian Grothoff + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include + + +/** + * Head of linked list for all loaded plugins + */ +static struct AuthPlugin *ap_head; + +/** + * Tail ofinked list for all loaded plugins + */ +static struct AuthPlugin *ap_tail; + + +/** + * Authentication plugin which is used to verify code based authentication + * like SMS, E-Mail. + */ +struct AuthPlugin +{ + /** + * Kept in a DLL. + */ + struct AuthPlugin *next; + + /** + * Kept in a DLL. + */ + struct AuthPlugin *prev; + + /** + * Actual plugin handle. + */ + struct ANASTASIS_AuthorizationPlugin *authorization; + + /** + * I.e. "sms", "phone". + */ + char *name; + + /** + * Name of the shared object providing the plugin logic. + */ + char *lib_name; + + /** + * Cost of using this plugin. + */ + struct TALER_Amount cost; +}; + + +struct ANASTASIS_AuthorizationPlugin * +ANASTASIS_authorization_plugin_load ( + const char *method, + const struct GNUNET_CONFIGURATION_Handle *AH_cfg, + struct TALER_Amount *cost) +{ + struct ANASTASIS_AuthorizationPlugin *authorization; + char *lib_name; + char *sec_name; + struct AuthPlugin *ap; + char *currency; + + for (ap = ap_head; NULL != ap; ap = ap->next) + if (0 == strcmp (method, + ap->name)) + { + *cost = ap->cost; + return ap->authorization; + } + if (GNUNET_OK != + TALER_config_get_currency (AH_cfg, + ¤cy)) + return NULL; + ap = GNUNET_new (struct AuthPlugin); + GNUNET_asprintf (&sec_name, + "authorization-%s", + method); + if (GNUNET_OK != + TALER_config_get_amount (AH_cfg, + sec_name, + "COST", + &ap->cost)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + sec_name, + "COST"); + GNUNET_free (sec_name); + GNUNET_free (currency); + GNUNET_free (ap); + return NULL; + } + + if (0 != + strcasecmp (currency, + ap->cost.currency)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + sec_name, + "COST", + "currency mismatch"); + GNUNET_free (currency); + GNUNET_free (sec_name); + GNUNET_free (ap); + return NULL; + } + GNUNET_free (currency); + GNUNET_free (sec_name); + GNUNET_asprintf (&lib_name, + "libanastasis_plugin_authorization_%s", + method); + authorization = GNUNET_PLUGIN_load (lib_name, + (void *) AH_cfg); + if (NULL == authorization) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Authentication method `%s' not supported\n", + method); + GNUNET_free (lib_name); + GNUNET_free (ap); + return NULL; + } + ap->name = GNUNET_strdup (method); + ap->lib_name = lib_name; + ap->authorization = authorization; + GNUNET_CONTAINER_DLL_insert (ap_head, + ap_tail, + ap); + *cost = ap->cost; + return authorization; +} + + +void +ANASTASIS_authorization_plugin_shutdown (void) +{ + struct AuthPlugin *ap; + + while (NULL != (ap = ap_head)) + { + GNUNET_CONTAINER_DLL_remove (ap_head, + ap_tail, + ap); + GNUNET_PLUGIN_unload (ap->lib_name, + ap->authorization); + GNUNET_free (ap->lib_name); + GNUNET_free (ap->name); + GNUNET_free (ap); + } +} + + +/** + * Libtool search path before we started. + */ +static char *old_dlsearchpath; + + +/** + * Setup libtool paths. + */ +void __attribute__ ((constructor)) +anastasis_authorization_plugin_init (void) +{ + int err; + const char *opath; + char *path; + char *cpath; + + err = lt_dlinit (); + if (err > 0) + { + fprintf (stderr, + _ ("Initialization of plugin mechanism failed: %s!\n"), + lt_dlerror ()); + return; + } + opath = lt_dlgetsearchpath (); + if (NULL != opath) + old_dlsearchpath = GNUNET_strdup (opath); + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_LIBDIR); + if (NULL != path) + { + if (NULL != opath) + { + GNUNET_asprintf (&cpath, "%s:%s", opath, path); + lt_dlsetsearchpath (cpath); + GNUNET_free (path); + GNUNET_free (cpath); + } + else + { + lt_dlsetsearchpath (path); + GNUNET_free (path); + } + } +} + + +/** + * Shutdown libtool. + */ +void __attribute__ ((destructor)) +anastasis_authorization_plugin_fini (void) +{ + lt_dlsetsearchpath (old_dlsearchpath); + if (NULL != old_dlsearchpath) + { + GNUNET_free (old_dlsearchpath); + } + lt_dlexit (); +} + + +/* end of anastasis_authorization_plugin.c */ diff --git a/src/authorization/anastasis_authorization_plugin_email.c b/src/authorization/anastasis_authorization_plugin_email.c new file mode 100644 index 0000000..33c400b --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_email.c @@ -0,0 +1,616 @@ +/* + This file is part of Anastasis + Copyright (C) 2019-2021 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file include/anastasis_authorization_plugin_email.c + * @brief authorization plugin email based + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include +#include +#include +#include "anastasis_util_lib.h" + + +/** + * Saves the State of a authorization plugin. + */ +struct Email_Context +{ + + /** + * Command which is executed to run the plugin (some bash script or a + * command line argument) + */ + char *auth_command; + + /** + * Regex for email address validation. + */ + regex_t regex; + + /** + * Messages of the plugin, read from a resource file. + */ + json_t *messages; + +}; + + +/** + * Saves the state of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * Public key of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Code which is sent to the user. + */ + uint64_t code; + + /** + * Our plugin context. + */ + struct Email_Context *ctx; + + /** + * Function to call when we made progress. + */ + GNUNET_SCHEDULER_TaskCallback trigger; + + /** + * Closure for @e trigger. + */ + void *trigger_cls; + + /** + * holds the truth information + */ + char *email; + + /** + * Handle to the helper process. + */ + struct GNUNET_OS_Process *child; + + /** + * Handle to wait for @e child + */ + struct GNUNET_ChildWaitHandle *cwh; + + /** + * Our client connection, set if suspended. + */ + struct MHD_Connection *connection; + + /** + * Message to send. + */ + char *msg; + + /** + * Offset of transmission in msg. + */ + size_t msg_off; + + /** + * Exit code from helper. + */ + long unsigned int exit_code; + + /** + * How did the helper die? + */ + enum GNUNET_OS_ProcessStatusType pst; + +}; + + +/** + * Obtain internationalized message @a msg_id from @a ctx using + * language preferences of @a conn. + * + * @param messages JSON object to lookup message from + * @param conn connection to lookup message for + * @param msg_id unique message ID + * @return NULL if message was not found + */ +static const char * +get_message (const json_t *messages, + struct MHD_Connection *conn, + const char *msg_id) +{ + const char *accept_lang; + + accept_lang = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == accept_lang) + accept_lang = "en_US"; + { + const char *ret; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_i18n_string (msg_id, + accept_lang, + &ret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (messages, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + return ret; + } +} + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed phone number for sending an SMS, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the phone number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +email_validate (void *cls, + struct MHD_Connection *connection, + const char *mime_type, + const char *data, + size_t data_length) +{ + struct Email_Context *ctx = cls; + int regex_result; + char *phone_number; + + phone_number = GNUNET_strndup (data, + data_length); + regex_result = regexec (&ctx->regex, + phone_number, + 0, + NULL, + 0); + GNUNET_free (phone_number); + if (0 != regex_result) + { + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_EMAIL_INVALID, + NULL)) + return GNUNET_SYSERR; + return GNUNET_NO; + } + return GNUNET_OK; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param cls closure + * @param trigger function to call when we made progress + * @param trigger_cls closure for @a trigger + * @param truth_uuid Identifier of the challenge, to be (if possible) included in the + * interaction with the user + * @param code secret code that the user has to provide back to satisfy the challenge in + * the main anastasis protocol + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @return state to track progress on the authorization operation, NULL on failure + */ +static struct ANASTASIS_AUTHORIZATION_State * +email_start (void *cls, + GNUNET_SCHEDULER_TaskCallback trigger, + void *trigger_cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code, + const void *data, + size_t data_length) +{ + struct Email_Context *ctx = cls; + struct ANASTASIS_AUTHORIZATION_State *as; + + as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); + as->trigger = trigger; + as->trigger_cls = trigger_cls; + as->ctx = ctx; + as->truth_uuid = *truth_uuid; + as->code = code; + as->email = GNUNET_strndup (data, + data_length); + return as; +} + + +/** + * Function called when our Email helper has terminated. + * + * @param cls our `struct ANASTASIS_AUHTORIZATION_State` + * @param type type of the process + * @param exit_code status code of the process + */ +static void +email_done_cb (void *cls, + enum GNUNET_OS_ProcessStatusType type, + long unsigned int exit_code) +{ + struct ANASTASIS_AUTHORIZATION_State *as = cls; + + as->child = NULL; + as->cwh = NULL; + as->pst = type; + as->exit_code = exit_code; + MHD_resume_connection (as->connection); + as->trigger (as->trigger_cls); +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param as authorization state + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +email_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection) +{ + MHD_RESULT mres; + const char *mime; + const char *lang; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + if (NULL == as->msg) + { + /* First time, start child process and feed pipe */ + struct GNUNET_DISK_PipeHandle *p; + struct GNUNET_DISK_FileHandle *pipe_stdin; + + p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); + if (NULL == p) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, + "pipe"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR, + p, + NULL, + NULL, + as->ctx->auth_command, + as->ctx->auth_command, + as->email, + NULL); + if (NULL == as->child) + { + GNUNET_DISK_pipe_close (p); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, + "exec"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + pipe_stdin = GNUNET_DISK_pipe_detach_end (p, + GNUNET_DISK_PIPE_END_WRITE); + GNUNET_assert (NULL != pipe_stdin); + GNUNET_DISK_pipe_close (p); + { + char *tpk; + + tpk = GNUNET_STRINGS_data_to_string_alloc ( + &as->truth_uuid, + sizeof (as->truth_uuid)); + GNUNET_asprintf (&as->msg, + get_message (as->ctx->messages, + connection, + "body"), + (unsigned long long) as->code, + tpk); + GNUNET_free (tpk); + } + + { + const char *off = as->msg; + size_t left = strlen (off); + + while (0 != left) + { + ssize_t ret; + + if (0 == left) + break; + ret = GNUNET_DISK_file_write (pipe_stdin, + off, + left); + if (ret <= 0) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, + "write"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->msg_off += ret; + off += ret; + left -= ret; + } + GNUNET_DISK_file_close (pipe_stdin); + } + as->cwh = GNUNET_wait_child (as->child, + &email_done_cb, + as); + as->connection = connection; + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if (NULL != as->cwh) + { + /* Spurious call, why are we here? */ + GNUNET_break (0); + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || + (0 != as->exit_code) ) + { + char es[32]; + + GNUNET_snprintf (es, + sizeof (es), + "%u/%d", + (unsigned int) as->exit_code, + as->pst); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_EMAIL_HELPER_COMMAND_FAILED, + es); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + + /* Build HTTP response */ + { + struct MHD_Response *resp; + const char *at; + size_t len; + + at = strchr (as->email, '@'); + if (NULL == at) + len = 0; + else + len = at - as->email; + + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + json_t *body; + char *user; + + user = GNUNET_strndup (as->email, + len); + body = json_pack ("{s:I, s:s, s:s}", + "code", + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, + "hint", + TALER_ErrorCode_get_hint ( + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED), + "detail", + user); + GNUNET_free (user); + GNUNET_break (NULL != body); + resp = TALER_MHD_make_json (body); + } + else + { + size_t reply_len; + char *reply; + + reply_len = GNUNET_asprintf (&reply, + get_message (as->ctx->messages, + connection, + "instructions"), + (unsigned int) len, + as->email); + resp = MHD_create_response_from_buffer (reply_len, + reply, + MHD_RESPMEM_MUST_COPY); + GNUNET_free (reply); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + } + mres = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; + } +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +email_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + if (NULL != as->cwh) + { + GNUNET_wait_child_cancel (as->cwh); + as->cwh = NULL; + } + if (NULL != as->child) + { + (void) GNUNET_OS_process_kill (as->child, + SIGKILL); + GNUNET_break (GNUNET_OK == + GNUNET_OS_process_wait (as->child)); + as->child = NULL; + } + GNUNET_free (as->msg); + GNUNET_free (as->email); + GNUNET_free (as); +} + + +/** + * Initialize email based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_email_init (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin; + struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct Email_Context *ctx; + + ctx = GNUNET_new (struct Email_Context); + { + char *fn; + json_error_t err; + + GNUNET_asprintf (&fn, + "%sauthorization-email-messages.json", + GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR)); + ctx->messages = json_load_file (fn, + JSON_REJECT_DUPLICATES, + &err); + if (NULL == ctx->messages) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load messages from `%s': %s at %d:%d\n", + fn, + err.text, + err.line, + err.column); + GNUNET_free (fn); + GNUNET_free (ctx); + return NULL; + } + GNUNET_free (fn); + } + { + int regex_result; + const char *regexp = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}"; + + regex_result = regcomp (&ctx->regex, + regexp, + REG_EXTENDED); + if (0 < regex_result) + { + GNUNET_break (0); + json_decref (ctx->messages); + GNUNET_free (ctx); + return NULL; + } + } + + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS; + plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS; + plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; + plugin->cls = ctx; + plugin->validate = &email_validate; + plugin->start = &email_start; + plugin->process = &email_process; + plugin->cleanup = &email_cleanup; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "authorization-email", + "COMMAND", + &ctx->auth_command)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-email", + "COMMAND"); + regfree (&ctx->regex); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; + } + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_email_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + struct Email_Context *ctx = plugin->cls; + + GNUNET_free (ctx->auth_command); + regfree (&ctx->regex); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; +} diff --git a/src/authorization/anastasis_authorization_plugin_file.c b/src/authorization/anastasis_authorization_plugin_file.c new file mode 100644 index 0000000..210ade7 --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_file.c @@ -0,0 +1,302 @@ +/* + This file is part of Anastasis + Copyright (C) 2019 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file include/anastasis_authorization_plugin_file.c + * @brief authorization plugin file based for testing + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include + + +/** + * Saves the state of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * UUID of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Code which is sent to the user (here saved into a file) + */ + uint64_t code; + + /** + * holds the truth information + */ + char *filename; + + /** + * closure + */ + void *cls; +}; + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed phone number for sending an SMS, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the phone number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +file_validate (void *cls, + struct MHD_Connection *connection, + const char *truth_mime, + const char *data, + size_t data_length) +{ + char *filename; + bool flag; + + if (NULL == data) + return GNUNET_NO; + filename = GNUNET_STRINGS_data_to_string_alloc (data, + data_length); + flag = false; + for (size_t i = 0; icls = cls; + as->truth_uuid = *truth_uuid; + as->code = code; + as->filename = GNUNET_strndup (data, + data_length); + return as; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param as authorization state + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +file_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection) +{ + const char *mime; + const char *lang; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + { + FILE *f = fopen (as->filename, "w"); + + if (NULL == f) + { + struct MHD_Response *resp; + MHD_RESULT mres; + + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + as->filename); + resp = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "open"); + mres = MHD_queue_response (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + + /* print challenge code to file */ + if (0 >= fprintf (f, + "%lu", + as->code)) + { + struct MHD_Response *resp; + MHD_RESULT mres; + + GNUNET_break (0 == fclose (f)); + resp = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "write"); + mres = MHD_queue_response (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + GNUNET_break (0 == fclose (f)); + } + + /* Build HTTP response */ + { + struct MHD_Response *resp; + + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + json_t *body; + + body = json_pack ("{s:s}", + "filename", + as->filename); + GNUNET_break (NULL != body); + resp = TALER_MHD_make_json (body); + } + else + { + size_t response_size; + char *response; + + response_size = GNUNET_asprintf (&response, + _ ("Challenge written to file")); + resp = MHD_create_response_from_buffer (response_size, + response, + MHD_RESPMEM_MUST_COPY); + GNUNET_free (response); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + } + + { + MHD_RESULT mres; + + mres = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; + } + } +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +file_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + GNUNET_free (as->filename); + GNUNET_free (as); +} + + +/** + * Initialize File based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_file_init (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin; + + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->code_validity_period = GNUNET_TIME_UNIT_MINUTES; + plugin->code_rotation_period = GNUNET_TIME_UNIT_MINUTES; + plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; + plugin->validate = &file_validate; + plugin->start = &file_start; + plugin->process = &file_process; + plugin->cleanup = &file_cleanup; + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_file_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + + GNUNET_free (plugin); + return NULL; +} diff --git a/src/authorization/anastasis_authorization_plugin_post.c b/src/authorization/anastasis_authorization_plugin_post.c new file mode 100644 index 0000000..1f20ff3 --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_post.c @@ -0,0 +1,655 @@ +/* + This file is part of Anastasis + Copyright (C) 2021 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file include/anastasis_authorization_plugin_post.c + * @brief authorization plugin post based + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include +#include +#include +#include "anastasis_util_lib.h" + + +/** + * Saves the State of a authorization plugin. + */ +struct PostContext +{ + + /** + * Command which is executed to run the plugin (some bash script or a + * command line argument) + */ + char *auth_command; + + /** + * Messages of the plugin, read from a resource file. + */ + json_t *messages; +}; + + +/** + * Saves the state of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * Public key of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Code which is sent to the user. + */ + uint64_t code; + + /** + * Our plugin context. + */ + struct PostContext *ctx; + + /** + * Function to call when we made progress. + */ + GNUNET_SCHEDULER_TaskCallback trigger; + + /** + * Closure for @e trigger. + */ + void *trigger_cls; + + /** + * holds the truth information + */ + json_t *post; + + /** + * Handle to the helper process. + */ + struct GNUNET_OS_Process *child; + + /** + * Handle to wait for @e child + */ + struct GNUNET_ChildWaitHandle *cwh; + + /** + * Our client connection, set if suspended. + */ + struct MHD_Connection *connection; + + /** + * Message to send. + */ + char *msg; + + /** + * Offset of transmission in msg. + */ + size_t msg_off; + + /** + * Exit code from helper. + */ + long unsigned int exit_code; + + /** + * How did the helper die? + */ + enum GNUNET_OS_ProcessStatusType pst; + + +}; + + +/** + * Obtain internationalized message @a msg_id from @a ctx using + * language preferences of @a conn. + * + * @param messages JSON object to lookup message from + * @param conn connection to lookup message for + * @param msg_id unique message ID + * @return NULL if message was not found + */ +static const char * +get_message (const json_t *messages, + struct MHD_Connection *conn, + const char *msg_id) +{ + const char *accept_lang; + + accept_lang = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == accept_lang) + accept_lang = "en_US"; + { + const char *ret; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_i18n_string (msg_id, + accept_lang, + &ret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (messages, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + return ret; + } +} + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed phone number for sending an SMS, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the phone number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +post_validate (void *cls, + struct MHD_Connection *connection, + const char *mime_type, + const char *data, + size_t data_length) +{ + struct PostContext *ctx = cls; + json_t *j; + json_error_t error; + const char *name; + const char *street; + const char *city; + const char *zip; + const char *country; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("full_name", + &name), + GNUNET_JSON_spec_string ("street", + &street), + GNUNET_JSON_spec_string ("city", + &city), + GNUNET_JSON_spec_string ("postcode", + &zip), + GNUNET_JSON_spec_string ("country", + &country), + GNUNET_JSON_spec_end () + }; + + (void) ctx; + j = json_loadb (data, + data_length, + JSON_REJECT_DUPLICATES, + &error); + if (NULL == j) + { + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_POST_INVALID, + "JSON malformed")) + return GNUNET_SYSERR; + return GNUNET_NO; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (j); + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_POST_INVALID, + "JSON lacked required address information")) + return GNUNET_SYSERR; + return GNUNET_NO; + } + json_decref (j); + return GNUNET_OK; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send mail. + * + * @param cls closure + * @param trigger function to call when we made progress + * @param trigger_cls closure for @a trigger + * @param truth_uuid Identifier of the challenge, to be (if possible) included in the + * interaction with the user + * @param code secret code that the user has to provide back to satisfy the challenge in + * the main anastasis protocol + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @return state to track progress on the authorization operation, NULL on failure + */ +static struct ANASTASIS_AUTHORIZATION_State * +post_start (void *cls, + GNUNET_SCHEDULER_TaskCallback trigger, + void *trigger_cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code, + const void *data, + size_t data_length) +{ + struct PostContext *ctx = cls; + struct ANASTASIS_AUTHORIZATION_State *as; + json_error_t error; + + as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); + as->trigger = trigger; + as->trigger_cls = trigger_cls; + as->ctx = ctx; + as->truth_uuid = *truth_uuid; + as->code = code; + as->post = json_loadb (data, + data_length, + JSON_REJECT_DUPLICATES, + &error); + if (NULL == as->post) + { + GNUNET_break (0); + GNUNET_free (as); + return NULL; + } + return as; +} + + +/** + * Function called when our Post helper has terminated. + * + * @param cls our `struct ANASTASIS_AUHTORIZATION_State` + * @param type type of the process + * @param exit_code status code of the process + */ +static void +post_done_cb (void *cls, + enum GNUNET_OS_ProcessStatusType type, + long unsigned int exit_code) +{ + struct ANASTASIS_AUTHORIZATION_State *as = cls; + + as->child = NULL; + as->cwh = NULL; + as->pst = type; + as->exit_code = exit_code; + MHD_resume_connection (as->connection); + as->trigger (as->trigger_cls); +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param as authorization state + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +post_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection) +{ + const char *mime; + const char *lang; + MHD_RESULT mres; + const char *name; + const char *street; + const char *city; + const char *zip; + const char *country; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("full_name", + &name), + GNUNET_JSON_spec_string ("street", + &street), + GNUNET_JSON_spec_string ("city", + &city), + GNUNET_JSON_spec_string ("postcode", + &zip), + GNUNET_JSON_spec_string ("country", + &country), + GNUNET_JSON_spec_end () + }; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + if (GNUNET_OK != + GNUNET_JSON_parse (as->post, + spec, + NULL, NULL)) + { + GNUNET_break (0); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_INVALID, + "address information incomplete"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + if (NULL == as->msg) + { + /* First time, start child process and feed pipe */ + struct GNUNET_DISK_PipeHandle *p; + struct GNUNET_DISK_FileHandle *pipe_stdin; + + p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); + if (NULL == p) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED, + "pipe"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR, + p, + NULL, + NULL, + as->ctx->auth_command, + as->ctx->auth_command, + name, + street, + city, + zip, + country, + NULL); + if (NULL == as->child) + { + GNUNET_DISK_pipe_close (p); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED, + "exec"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + pipe_stdin = GNUNET_DISK_pipe_detach_end (p, + GNUNET_DISK_PIPE_END_WRITE); + GNUNET_assert (NULL != pipe_stdin); + GNUNET_DISK_pipe_close (p); + { + char *tpk; + + tpk = GNUNET_STRINGS_data_to_string_alloc ( + &as->truth_uuid, + sizeof (as->truth_uuid)); + GNUNET_asprintf (&as->msg, + get_message (as->ctx->messages, + connection, + "body"), + (unsigned long long) as->code, + tpk); + GNUNET_free (tpk); + } + + { + const char *off = as->msg; + size_t left = strlen (off); + + while (0 != left) + { + ssize_t ret; + + if (0 == left) + break; + ret = GNUNET_DISK_file_write (pipe_stdin, + off, + left); + if (ret <= 0) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED, + "write"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->msg_off += ret; + off += ret; + left -= ret; + } + GNUNET_DISK_file_close (pipe_stdin); + } + as->cwh = GNUNET_wait_child (as->child, + &post_done_cb, + as); + as->connection = connection; + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if (NULL != as->cwh) + { + /* Spurious call, why are we here? */ + GNUNET_break (0); + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || + (0 != as->exit_code) ) + { + char es[32]; + + GNUNET_snprintf (es, + sizeof (es), + "%u/%d", + (unsigned int) as->exit_code, + as->pst); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_COMMAND_FAILED, + es); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + + /* Build HTTP response */ + { + struct MHD_Response *resp; + + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + json_t *body; + + body = json_pack ("{s:I, s:s, s:s}", + "code", + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, + "hint", + TALER_ErrorCode_get_hint ( + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED), + "detail", + zip); + GNUNET_break (NULL != body); + resp = TALER_MHD_make_json (body); + } + else + { + size_t reply_len; + char *reply; + + reply_len = GNUNET_asprintf (&reply, + get_message (as->ctx->messages, + connection, + "instructions"), + zip); + resp = MHD_create_response_from_buffer (reply_len, + reply, + MHD_RESPMEM_MUST_COPY); + GNUNET_free (reply); + TALER_MHD_add_global_headers (resp); + } + mres = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; + } +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +post_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + if (NULL != as->cwh) + { + GNUNET_wait_child_cancel (as->cwh); + as->cwh = NULL; + } + if (NULL != as->child) + { + (void) GNUNET_OS_process_kill (as->child, + SIGKILL); + GNUNET_break (GNUNET_OK == + GNUNET_OS_process_wait (as->child)); + as->child = NULL; + } + GNUNET_free (as->msg); + json_decref (as->post); + GNUNET_free (as); +} + + +/** + * Initialize post based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_post_init (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin; + struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct PostContext *ctx; + + ctx = GNUNET_new (struct PostContext); + { + char *fn; + json_error_t err; + + GNUNET_asprintf (&fn, + "%sauthorization-post-messages.json", + GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR)); + ctx->messages = json_load_file (fn, + JSON_REJECT_DUPLICATES, + &err); + if (NULL == ctx->messages) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load messages from `%s': %s at %d:%d\n", + fn, + err.text, + err.line, + err.column); + GNUNET_free (fn); + GNUNET_free (ctx); + return NULL; + } + GNUNET_free (fn); + } + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->code_validity_period = GNUNET_TIME_UNIT_MONTHS; + plugin->code_rotation_period = GNUNET_TIME_UNIT_WEEKS; + plugin->code_retransmission_frequency + = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_DAYS, + 2); + plugin->cls = ctx; + plugin->validate = &post_validate; + plugin->start = &post_start; + plugin->process = &post_process; + plugin->cleanup = &post_cleanup; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "authorization-post", + "COMMAND", + &ctx->auth_command)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-post", + "COMMAND"); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; + } + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_post_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + struct PostContext *ctx = plugin->cls; + + GNUNET_free (ctx->auth_command); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; +} diff --git a/src/authorization/anastasis_authorization_plugin_sms.c b/src/authorization/anastasis_authorization_plugin_sms.c new file mode 100644 index 0000000..01b5f73 --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_sms.c @@ -0,0 +1,607 @@ +/* + This file is part of Anastasis + Copyright (C) 2019, 2021 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file include/anastasis_authorization_plugin_email.c + * @brief authorization plugin email based + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include +#include +#include +#include "anastasis_util_lib.h" + + +/** + * Saves the State of a authorization plugin. + */ +struct SMS_Context +{ + + /** + * Command which is executed to run the plugin (some bash script or a + * command line argument) + */ + char *auth_command; + + /** + * Regex for phone number validation. + */ + regex_t regex; + + /** + * Messages of the plugin, read from a resource file. + */ + json_t *messages; +}; + + +/** + * Saves the State of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * Public key of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Code which is sent to the user (here sent via SMS) + */ + uint64_t code; + + /** + * Our plugin context. + */ + struct SMS_Context *ctx; + + /** + * Function to call when we made progress. + */ + GNUNET_SCHEDULER_TaskCallback trigger; + + /** + * Closure for @e trigger. + */ + void *trigger_cls; + + /** + * holds the truth information + */ + char *phone_number; + + /** + * Handle to the helper process. + */ + struct GNUNET_OS_Process *child; + + /** + * Handle to wait for @e child + */ + struct GNUNET_ChildWaitHandle *cwh; + + /** + * Our client connection, set if suspended. + */ + struct MHD_Connection *connection; + + /** + * Message to send. + */ + char *msg; + + /** + * Offset of transmission in msg. + */ + size_t msg_off; + + /** + * Exit code from helper. + */ + long unsigned int exit_code; + + /** + * How did the helper die? + */ + enum GNUNET_OS_ProcessStatusType pst; + +}; + + +/** + * Obtain internationalized message @a msg_id from @a ctx using + * language preferences of @a conn. + * + * @param messages JSON object to lookup message from + * @param conn connection to lookup message for + * @param msg_id unique message ID + * @return NULL if message was not found + */ +static const char * +get_message (const json_t *messages, + struct MHD_Connection *conn, + const char *msg_id) +{ + const char *accept_lang; + + accept_lang = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == accept_lang) + accept_lang = "en_US"; + { + const char *ret; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_i18n_string (msg_id, + accept_lang, + &ret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (messages, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + return ret; + } +} + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed phone number for sending an SMS, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the phone number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure with a `struct SMS_Context` + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +sms_validate (void *cls, + struct MHD_Connection *connection, + const char *truth_mime, + const char *data, + size_t data_length) +{ + struct SMS_Context *ctx = cls; + int regex_result; + char *phone_number; + + phone_number = GNUNET_strndup (data, + data_length); + regex_result = regexec (&ctx->regex, + phone_number, + 0, + NULL, + 0); + GNUNET_free (phone_number); + if (0 != regex_result) + { + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_SMS_PHONE_INVALID, + NULL)) + return GNUNET_SYSERR; + return GNUNET_NO; + } + return GNUNET_OK; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * Sends SMS. + * + * @param cls closure with a `struct SMS_Context` + * @param trigger function to call when we made progress + * @param trigger_cls closure for @a trigger + * @param truth_uuid Identifier of the challenge, to be (if possible) included in the + * interaction with the user + * @param code secret code that the user has to provide back to satisfy the challenge in + * the main anastasis protocol + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @return state to track progress on the authorization operation, NULL on failure + */ +static struct ANASTASIS_AUTHORIZATION_State * +sms_start (void *cls, + GNUNET_SCHEDULER_TaskCallback trigger, + void *trigger_cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code, + const void *data, + size_t data_length) +{ + struct SMS_Context *ctx = cls; + struct ANASTASIS_AUTHORIZATION_State *as; + + as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); + as->trigger = trigger; + as->trigger_cls = trigger_cls; + as->ctx = ctx; + as->truth_uuid = *truth_uuid; + as->code = code; + as->phone_number = GNUNET_strndup (data, + data_length); + return as; +} + + +/** + * Function called when our SMS helper has terminated. + * + * @param cls our `struct ANASTASIS_AUHTORIZATION_State` + * @param type type of the process + * @param exit_code status code of the process + */ +static void +sms_done_cb (void *cls, + enum GNUNET_OS_ProcessStatusType type, + long unsigned int exit_code) +{ + struct ANASTASIS_AUTHORIZATION_State *as = cls; + + as->child = NULL; + as->cwh = NULL; + as->pst = type; + as->exit_code = exit_code; + MHD_resume_connection (as->connection); + as->trigger (as->trigger_cls); +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param as authorization state + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +sms_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection) +{ + MHD_RESULT mres; + const char *mime; + const char *lang; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + if (NULL == as->msg) + { + /* First time, start child process and feed pipe */ + struct GNUNET_DISK_PipeHandle *p; + struct GNUNET_DISK_FileHandle *pipe_stdin; + + p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); + if (NULL == p) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, + "pipe"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR, + p, + NULL, + NULL, + as->ctx->auth_command, + as->ctx->auth_command, + as->phone_number, + NULL); + if (NULL == as->child) + { + GNUNET_DISK_pipe_close (p); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, + "exec"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + pipe_stdin = GNUNET_DISK_pipe_detach_end (p, + GNUNET_DISK_PIPE_END_WRITE); + GNUNET_assert (NULL != pipe_stdin); + GNUNET_DISK_pipe_close (p); + { + char *tpk; + + tpk = GNUNET_STRINGS_data_to_string_alloc ( + &as->truth_uuid, + sizeof (as->truth_uuid)); + GNUNET_asprintf (&as->msg, + "A-%llu\nAnastasis\n%s", + (unsigned long long) as->code, + tpk); + GNUNET_free (tpk); + } + + { + const char *off = as->msg; + size_t left = strlen (off); + + while (0 != left) + { + ssize_t ret; + + if (0 == left) + break; + ret = GNUNET_DISK_file_write (pipe_stdin, + off, + left); + if (ret <= 0) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, + "write"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->msg_off += ret; + off += ret; + left -= ret; + } + GNUNET_DISK_file_close (pipe_stdin); + } + as->cwh = GNUNET_wait_child (as->child, + &sms_done_cb, + as); + as->connection = connection; + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if (NULL != as->cwh) + { + /* Spurious call, why are we here? */ + GNUNET_break (0); + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || + (0 != as->exit_code) ) + { + char es[32]; + + GNUNET_snprintf (es, + sizeof (es), + "%u/%d", + (unsigned int) as->exit_code, + as->pst); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_SMS_HELPER_COMMAND_FAILED, + es); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + + /* Build HTTP response */ + { + struct MHD_Response *resp; + const char *end; + size_t slen; + + slen = strlen (as->phone_number); + if (slen > 4) + end = &as->phone_number[slen - 4]; + else + end = &as->phone_number[slen / 2]; + + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + json_t *body; + + body = json_pack ("{s:I, s:s, s:s}", + "code", + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, + "hint", + TALER_ErrorCode_get_hint ( + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED), + "detail", + end); + GNUNET_break (NULL != body); + resp = TALER_MHD_make_json (body); + } + else + { + size_t reply_len; + char *reply; + + reply_len = GNUNET_asprintf (&reply, + get_message (as->ctx->messages, + connection, + "instructions"), + end); + resp = MHD_create_response_from_buffer (reply_len, + reply, + MHD_RESPMEM_MUST_COPY); + GNUNET_free (reply); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + } + mres = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; + } +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +sms_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + if (NULL != as->cwh) + { + GNUNET_wait_child_cancel (as->cwh); + as->cwh = NULL; + } + if (NULL != as->child) + { + (void) GNUNET_OS_process_kill (as->child, + SIGKILL); + GNUNET_break (GNUNET_OK == + GNUNET_OS_process_wait (as->child)); + as->child = NULL; + } + GNUNET_free (as->msg); + GNUNET_free (as->phone_number); + GNUNET_free (as); +} + + +/** + * Initialize email based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_sms_init (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin; + struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct SMS_Context *ctx; + + ctx = GNUNET_new (struct SMS_Context); + { + char *fn; + json_error_t err; + + GNUNET_asprintf (&fn, + "%sauthorization-sms-messages.json", + GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR)); + ctx->messages = json_load_file (fn, + JSON_REJECT_DUPLICATES, + &err); + if (NULL == ctx->messages) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load messages from `%s': %s at %d:%d\n", + fn, + err.text, + err.line, + err.column); + GNUNET_free (fn); + GNUNET_free (ctx); + return NULL; + } + GNUNET_free (fn); + } + { + int regex_result; + const char *regexp = "^\\+?[0-9]+$"; + + regex_result = regcomp (&ctx->regex, + regexp, + REG_EXTENDED); + if (0 != regex_result) + { + GNUNET_break (0); + json_decref (ctx->messages); + GNUNET_free (ctx); + return NULL; + } + } + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS; + plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS; + plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; + plugin->cls = ctx; + plugin->validate = &sms_validate; + plugin->start = &sms_start; + plugin->process = &sms_process; + plugin->cleanup = &sms_cleanup; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "authorization-sms", + "COMMAND", + &ctx->auth_command)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-sms", + "COMMAND"); + regfree (&ctx->regex); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; + } + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_sms_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + struct SMS_Context *ctx = plugin->cls; + + GNUNET_free (ctx->auth_command); + regfree (&ctx->regex); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; +} diff --git a/src/authorization/authorization-email-messages.json b/src/authorization/authorization-email-messages.json new file mode 100644 index 0000000..56f648c --- /dev/null +++ b/src/authorization/authorization-email-messages.json @@ -0,0 +1,10 @@ +{ + "instructions" : "Recovery TAN was sent to email %.*s@DOMAIN", + "instructions_i18n" : { + "de_DE" : "Ein Authorisierungscode wurde an %.*s@DOMAIN geschickt" + }, + "body" : "Subject: Anastasis recovery code: A-%llu\n\nThis is for challenge %s.\n", + "body_i18n" : { + "de_DE" : "Subject: Anastasis Autorisierungscode: A-%llu\n\nDies ist der Code für den Vorgang %s.\n" + } +} diff --git a/src/authorization/authorization-post-messages.json b/src/authorization/authorization-post-messages.json new file mode 100644 index 0000000..d2ac83a --- /dev/null +++ b/src/authorization/authorization-post-messages.json @@ -0,0 +1,7 @@ +{ + "instructions" : "Recovery message send to an address with ZIP code %s", + "instructions_i18n" : { + "de_DE" : "Ein Authorisierungscode wurde an eine Addresse mit der Postleitzahl %s geschickt" + }, + "body" : "Dear Customer\n\nThe Anastasis recovery code you need to\nrecover your data is A-%llu.\nThis is for challenge %s.\n\nBest regards\n\nYour Anastasis provider" +} diff --git a/src/authorization/authorization-sms-messages.json b/src/authorization/authorization-sms-messages.json new file mode 100644 index 0000000..cb45ed8 --- /dev/null +++ b/src/authorization/authorization-sms-messages.json @@ -0,0 +1,6 @@ +{ + "instructions" : "Recovery TAN send to phone number ending with %s", + "instructions_i18n" : { + "de_DE" : "Ein Authorisierungscode wurde an die Telefonnummer mit der Endung %s geschickt" + } +} 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 +*/ +/** + * @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", + "404: not found", 0, + &TMH_MHD_handler_static_response, MHD_HTTP_NOT_FOUND + }; + static struct AH_RequestHandler h405 = { + "", NULL, "text/html", + "405: method not allowed", 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 +*/ +/** + * @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 +#include +#include + + +/** + * 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 +*/ +/** + * @file backend/anastasis-httpd_config.c + * @brief headers for /terms handler + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include +#include "anastasis-httpd_config.h" +#include "anastasis-httpd.h" +#include +#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 +*/ +/** + * @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 +#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 +*/ +/** + * @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 +#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 +*/ + +/** + * @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 +#include +#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 +*/ +/** + * @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 +#include +#include +#include +#include + +/** + * 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 +*/ +/** + * @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 + + +/** + * 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 +*/ +/** + * @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 +#include +#include +#include +#include + +/** + * 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 +*/ +/** + * @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 + +/** + * 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 +*/ +/** + * @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 +#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 +*/ +/** + * @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 +#include +#include "anastasis_authorization_lib.h" +#include +#include + +/** + * 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 +*/ +/** + * @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 + + +/** + * 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 +*/ +/** + * @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 +#include +#include +#include +#include +#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 +*/ +/** + * @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 +#include +#include "anastasis_redux.h" +#include +#include +#include +#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 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 /dev/null /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 /dev/null /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 +*/ +/** + * @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 +#include +#include + + +/* ********************* 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 +*/ +/** + * @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 +*/ +/** + * @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 + + +/** + * 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 +*/ +/** + * @file lib/anastasis_cryto_lib.h + * @brief anastasis crypto api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include +#include + + +/** + * 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 +*/ +/** + * @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 +*/ +/** + * @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 + +/** + * 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 +*/ +/** + * @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 +#include +#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 +*/ +/** + * @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 +#include "anastasis.h" +#include +#include + + +/** + * 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 +*/ +/** + * @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 +#include + + +/** + * 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 +*/ +/** + * @file include/anastasis_testing_lib.h + * @brief API for writing an interpreter to test Taler components + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#ifndef ANASTASIS_TESTING_LIB_H +#define ANASTASIS_TESTING_LIB_H + +#include "anastasis.h" +#include +#include + +/* ********************* 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 +*/ +/** + * @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 +#include + + +/** + * 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 . + 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 + +#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 a NOP. We don't include + as well because people using "gettext.h" will not include , + and also including would fail on SunOS 4, whereas + is GNUNET_OK. */ +#if defined(__sun) +#include +#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 +*/ + +/** + * @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 + */ + +#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 + +/* 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 +*/ +/** + * @brief anastasis client api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis.h" +#include +#include + + +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; itruths[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; itruths_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; ipss_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; ipss_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; ipss_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 +*/ +/** + * @brief anastasis client api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis.h" +#include +#include +#include +#include + + +/** + * 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, + json_error.source, + json_error.position); + GNUNET_break_op (0); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_DOWNLOAD_NO_JSON, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + } + + { + const char *secret_name = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("policies", + &dec_policies), + GNUNET_JSON_spec_json ("escrow_methods", + &esc_methods), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("secret_name", + &secret_name)), + GNUNET_JSON_spec_varsize ("encrypted_core_secret", + &r->enc_core_secret, + &r->enc_core_secret_size), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (recovery_document, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + json_dumpf (recovery_document, + stderr, + 0); + json_decref (recovery_document); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_MALFORMED_JSON, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + } + if (NULL != secret_name) + { + GNUNET_break (NULL == r->secret_name); + r->secret_name = GNUNET_strdup (secret_name); + r->ri.secret_name = r->secret_name; + } + } + json_decref (recovery_document); + } + + r->ri.version = dd->version; + r->ri.cs_len = json_array_size (esc_methods); + r->ri.dps_len = json_array_size (dec_policies); + r->ri.dps = GNUNET_new_array (r->ri.dps_len, + struct ANASTASIS_DecryptionPolicy *); + r->dps = GNUNET_new_array (r->ri.dps_len, + struct DecryptionPolicy); + r->solved_challenges = GNUNET_new_array (r->ri.cs_len, + struct ANASTASIS_Challenge *); + r->ri.cs = GNUNET_new_array (r->ri.cs_len, + struct ANASTASIS_Challenge *); + r->cs = GNUNET_new_array (r->ri.cs_len, + struct ANASTASIS_Challenge); + for (unsigned int i = 0; i < r->ri.cs_len; i++) + { + struct ANASTASIS_Challenge *cs = &r->cs[i]; + const char *instructions; + const char *url; + const char *escrow_type; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("uuid", + &cs->ci.uuid), + GNUNET_JSON_spec_string ("url", + &url), + GNUNET_JSON_spec_string ("instructions", + &instructions), + GNUNET_JSON_spec_fixed_auto ("truth_key", + &cs->truth_key), + GNUNET_JSON_spec_fixed_auto ("salt", + &cs->salt), + GNUNET_JSON_spec_fixed_auto ("provider_salt", + &cs->provider_salt), + GNUNET_JSON_spec_string ("escrow_type", + &escrow_type), + GNUNET_JSON_spec_end () + }; + + r->ri.cs[i] = cs; + cs->recovery = r; + if (GNUNET_OK != + GNUNET_JSON_parse (json_array_get (esc_methods, + i), + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + json_decref (esc_methods); + json_decref (dec_policies); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_MALFORMED_JSON, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + } + cs->url = GNUNET_strdup (url); + cs->type = GNUNET_strdup (escrow_type); + cs->ci.type = cs->type; + cs->ci.provider_url = cs->url; + cs->instructions = GNUNET_strdup (instructions); + cs->ci.instructions = cs->instructions; + } + json_decref (esc_methods); + + for (unsigned int j = 0; j < r->ri.dps_len; j++) + { + struct DecryptionPolicy *dp = &r->dps[j]; + json_t *uuids = NULL; + json_t *uuid; + size_t n_index; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("master_key", + &dp->emk), + GNUNET_JSON_spec_fixed_auto ("salt", + &dp->salt), + GNUNET_JSON_spec_json ("uuids", + &uuids), + GNUNET_JSON_spec_end () + }; + + r->ri.dps[j] = &r->dps[j].pub_details; + if ( (GNUNET_OK != + GNUNET_JSON_parse (json_array_get (dec_policies, + j), + spec, + NULL, NULL)) || + (! json_is_array (uuids)) ) + { + GNUNET_break_op (0); + json_decref (uuids); + json_decref (dec_policies); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_MALFORMED_JSON, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + } + + dp->pub_details.challenges_length = json_array_size (uuids); + dp->pub_details.challenges + = GNUNET_new_array (dp->pub_details.challenges_length, + struct ANASTASIS_Challenge *); + json_array_foreach (uuids, n_index, uuid) + { + const char *uuid_str = json_string_value (uuid); + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + bool found = false; + + if ( (NULL == uuid_str) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + uuid_str, + strlen (uuid_str), + &uuid, + sizeof (uuid))) ) + { + GNUNET_break_op (0); + json_decref (dec_policies); + json_decref (uuids); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_MALFORMED_JSON, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + } + for (unsigned int i = 0; iri.cs_len; i++) + { + if (0 != + GNUNET_memcmp (&uuid, + &r->cs[i].ci.uuid)) + continue; + found = true; + dp->pub_details.challenges[n_index] = &r->cs[i]; + break; + } + if (! found) + { + GNUNET_break_op (0); + json_decref (dec_policies); + json_decref (uuids); + r->csc (r->csc_cls, + ANASTASIS_RS_POLICY_MALFORMED_JSON, + NULL, + 0); + ANASTASIS_recovery_abort (r); + return; + } + } + json_decref (uuids); + } + json_decref (dec_policies); + r->pc (r->pc_cls, + &r->ri); +} + + +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) +{ + struct ANASTASIS_Recovery *r; + struct ANASTASIS_CRYPTO_AccountPublicKeyP pub_key; + + r = GNUNET_new (struct ANASTASIS_Recovery); + r->csc = csc; + r->csc_cls = csc_cls; + r->pc = pc; + r->pc_cls = pc_cls; + r->ctx = ctx; + r->id_data = json_incref ((json_t *) id_data); + r->provider_url = GNUNET_strdup (anastasis_provider_url); + ANASTASIS_CRYPTO_user_identifier_derive (id_data, + provider_salt, + &r->id); + ANASTASIS_CRYPTO_account_public_key_derive (&r->id, + &pub_key); + r->ri.version = version; + if (0 != version) + { + r->plo = ANASTASIS_policy_lookup_version (r->ctx, + anastasis_provider_url, + &pub_key, + &policy_lookup_cb, + r, + version); + } + else + { + r->plo = ANASTASIS_policy_lookup (r->ctx, + anastasis_provider_url, + &pub_key, + &policy_lookup_cb, + r); + } + if (NULL == r->plo) + { + GNUNET_break (0); + ANASTASIS_recovery_abort (r); + return NULL; + } + return r; +} + + +json_t * +ANASTASIS_recovery_serialize (const struct ANASTASIS_Recovery *r) +{ + json_t *dps_arr; + json_t *cs_arr; + + dps_arr = json_array (); + GNUNET_assert (NULL != dps_arr); + for (unsigned int i = 0; iri.dps_len; i++) + { + const struct DecryptionPolicy *dp = &r->dps[i]; + json_t *c_arr; + json_t *dps; + + c_arr = json_array (); + GNUNET_assert (NULL != c_arr); + for (unsigned int j = 0; j < dp->pub_details.challenges_length; j++) + { + const struct ANASTASIS_Challenge *c = dp->pub_details.challenges[j]; + json_t *cs; + + cs = json_pack ("{s:o}", + "uuid", + GNUNET_JSON_from_data_auto (&c->ci.uuid)); + GNUNET_assert (NULL != cs); + GNUNET_assert (0 == + json_array_append_new (c_arr, + cs)); + } + dps = json_pack ("{s:o, s:o, s:o}", + "emk", + GNUNET_JSON_from_data_auto (&dp->emk), + "salt", + GNUNET_JSON_from_data_auto (&dp->salt), + "challenges", + c_arr); + GNUNET_assert (NULL != dps); + GNUNET_assert (0 == + json_array_append_new (dps_arr, + dps)); + } + cs_arr = json_array (); + GNUNET_assert (NULL != cs_arr); + for (unsigned int i = 0; iri.cs_len; i++) + { + const struct ANASTASIS_Challenge *c = &r->cs[i]; + json_t *cs; + + cs = json_pack ("{s:o,s:o,s:o,s:o,s:o?," + " s:s,s:s,s:s,s:b}", + "uuid", + GNUNET_JSON_from_data_auto (&c->ci.uuid), + "truth_key", + GNUNET_JSON_from_data_auto (&c->truth_key), + "salt", + GNUNET_JSON_from_data_auto (&c->salt), + "provider_salt", + GNUNET_JSON_from_data_auto (&c->provider_salt), + "key_share", + c->ci.solved + ? GNUNET_JSON_from_data_auto (&c->key_share) + : NULL, + "url", + c->url, + "type", + c->type, + "instructions", + c->instructions, + "solved", + c->ci.solved); + GNUNET_assert (NULL != cs); + GNUNET_assert (0 == + json_array_append_new (cs_arr, + cs)); + } + + return json_pack ("{s:o, s:o, s:o, s:I, s:O, " + " s:s, s:s?, s:o}", + "id", + GNUNET_JSON_from_data_auto (&r->id), + "dps", + dps_arr, + "cs", + cs_arr, + "version", + (json_int_t) r->ri.version, + "id_data", + r->id_data, + "provider_url", + r->provider_url, + "secret_name", + r->secret_name, + "core_secret", + GNUNET_JSON_from_data (r->enc_core_secret, + r->enc_core_secret_size)); +} + + +/** + * Parse the @a cs_array and update @a r accordingly + * + * @param[in,out] r recovery information to update + * @param cs_arr serialized data to parse + * @return #GNUNET_OK on success + */ +static int +parse_cs_array (struct ANASTASIS_Recovery *r, + json_t *cs_arr) +{ + json_t *cs; + unsigned int n_index; + + if (! json_is_array (cs_arr)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + r->ri.cs_len = json_array_size (cs_arr); + r->solved_challenges = GNUNET_new_array (r->ri.cs_len, + struct ANASTASIS_Challenge *); + r->ri.cs = GNUNET_new_array (r->ri.cs_len, + struct ANASTASIS_Challenge *); + r->cs = GNUNET_new_array (r->ri.cs_len, + struct ANASTASIS_Challenge); + json_array_foreach (cs_arr, n_index, cs) + { + struct ANASTASIS_Challenge *c = &r->cs[n_index]; + const char *instructions; + const char *url; + const char *escrow_type; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("uuid", + &c->ci.uuid), + GNUNET_JSON_spec_string ("url", + &url), + GNUNET_JSON_spec_string ("instructions", + &instructions), + GNUNET_JSON_spec_fixed_auto ("truth_key", + &c->truth_key), + GNUNET_JSON_spec_fixed_auto ("salt", + &c->salt), + GNUNET_JSON_spec_fixed_auto ("provider_salt", + &c->provider_salt), + GNUNET_JSON_spec_string ("type", + &escrow_type), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("key_share", + &c->key_share)), + GNUNET_JSON_spec_end () + }; + + r->ri.cs[n_index] = c; + c->recovery = r; + if (GNUNET_OK != + GNUNET_JSON_parse (cs, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + c->url = GNUNET_strdup (url); + c->type = GNUNET_strdup (escrow_type); + c->ci.type = c->type; + c->instructions = GNUNET_strdup (instructions); + c->ci.instructions = c->instructions; + c->ci.provider_url = c->url; + { + json_t *ks; + + ks = json_object_get (cs, + "key_share"); + if ( (NULL != ks) && + (! json_is_null (ks)) ) + { + c->ci.solved = true; + r->solved_challenges[r->solved_challenge_pos++] = c; + } + } + } + + return GNUNET_OK; +} + + +/** + * Parse the @a dps_array and update @a r accordingly + * + * @param[in,out] r recovery information to update + * @param dps_arr serialized data to parse + * @return #GNUNET_OK on success + */ +static int +parse_dps_array (struct ANASTASIS_Recovery *r, + json_t *dps_arr) +{ + json_t *dps; + unsigned int n_index; + + if (! json_is_array (dps_arr)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + r->ri.dps_len = json_array_size (dps_arr); + r->dps = GNUNET_new_array (r->ri.dps_len, + struct DecryptionPolicy); + r->ri.dps = GNUNET_new_array (r->ri.dps_len, + struct ANASTASIS_DecryptionPolicy *); + + json_array_foreach (dps_arr, n_index, dps) + { + struct DecryptionPolicy *dp = &r->dps[n_index]; + json_t *challenges; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("emk", + &dp->emk), + GNUNET_JSON_spec_fixed_auto ("salt", + &dp->salt), + GNUNET_JSON_spec_json ("challenges", + &challenges), + GNUNET_JSON_spec_end () + }; + const char *err_json_name; + unsigned int err_line; + + r->ri.dps[n_index] = &dp->pub_details; + if (GNUNET_OK != + GNUNET_JSON_parse (dps, + spec, + &err_json_name, + &err_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse decryption policy JSON entry `%s'\n", + err_json_name); + json_dumpf (dps, + stderr, + JSON_INDENT (2)); + return GNUNET_SYSERR; + } + if (! json_is_array (challenges)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return GNUNET_SYSERR; + } + dp->pub_details.challenges_length = json_array_size (challenges); + dp->pub_details.challenges = GNUNET_new_array ( + dp->pub_details.challenges_length, + struct ANASTASIS_Challenge *); + + { + json_t *challenge; + unsigned int c_index; + json_array_foreach (challenges, c_index, challenge) + { + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ("uuid", + &uuid), + GNUNET_JSON_spec_end () + }; + bool found = false; + + if (GNUNET_OK != + GNUNET_JSON_parse (challenge, + ispec, + NULL, NULL)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return GNUNET_SYSERR; + } + for (unsigned int i = 0; iri.cs_len; i++) + { + if (0 != + GNUNET_memcmp (&uuid, + &r->cs[i].ci.uuid)) + continue; + dp->pub_details.challenges[c_index] = &r->cs[i]; + found = true; + } + if (! found) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return GNUNET_SYSERR; + } + } + } + GNUNET_JSON_parse_free (spec); + } + return GNUNET_OK; +} + + +/** + * Asynchronously call "pc" on the recovery information. + * + * @param cls a `struct ANASTASIS_Recovery *` + */ +static void +run_async_pc (void *cls) +{ + struct ANASTASIS_Recovery *r = cls; + + r->do_async = NULL; + r->pc (r->pc_cls, + &r->ri); +} + + +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) +{ + struct ANASTASIS_Recovery *r; + + r = GNUNET_new (struct ANASTASIS_Recovery); + r->csc = csc; + r->csc_cls = csc_cls; + r->pc = pc; + r->pc_cls = pc_cls; + r->ctx = ctx; + { + const char *err_json_name; + unsigned int err_line; + uint32_t version; + json_t *dps_arr; + json_t *cs_arr; + json_t *id_data; + const char *provider_url; + const char *secret_name; + void *ecs; + size_t ecs_size; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("id", + &r->id), + GNUNET_JSON_spec_string ("provider_url", + &provider_url), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("secret_name", + &secret_name)), + GNUNET_JSON_spec_uint32 ("version", + &version), + GNUNET_JSON_spec_json ("dps", + &dps_arr), + GNUNET_JSON_spec_json ("cs", + &cs_arr), + GNUNET_JSON_spec_json ("id_data", + &id_data), + GNUNET_JSON_spec_varsize ("core_secret", + &ecs, + &ecs_size), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (input, + spec, + &err_json_name, + &err_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse recovery document JSON entry `%s'\n", + err_json_name); + json_dumpf (input, + stderr, + JSON_INDENT (2)); + return NULL; + } + r->ri.version = version; + if ( (GNUNET_OK != + parse_cs_array (r, + cs_arr)) || + (GNUNET_OK != + parse_dps_array (r, + dps_arr)) ) + { + GNUNET_break_op (0); + ANASTASIS_recovery_abort (r); + GNUNET_JSON_parse_free (spec); + return NULL; + } + r->id_data = json_incref (id_data); + r->provider_url = GNUNET_strdup (provider_url); + if (NULL != secret_name) + r->secret_name = GNUNET_strdup (secret_name); + r->ri.secret_name = r->secret_name; + if (0 != ecs_size) + { + r->enc_core_secret = GNUNET_memdup (ecs, + ecs_size); + r->enc_core_secret_size = ecs_size; + } + GNUNET_JSON_parse_free (spec); + } + if (0 == r->ri.dps_len) + { + struct ANASTASIS_CRYPTO_AccountPublicKeyP pub_key; + + ANASTASIS_CRYPTO_account_public_key_derive (&r->id, + &pub_key); + if (0 != r->ri.version) + { + r->plo = ANASTASIS_policy_lookup_version (r->ctx, + r->provider_url, + &pub_key, + &policy_lookup_cb, + r, + r->ri.version); + } + else + { + r->plo = ANASTASIS_policy_lookup (r->ctx, + r->provider_url, + &pub_key, + &policy_lookup_cb, + r); + } + if (NULL == r->plo) + { + GNUNET_break (0); + ANASTASIS_recovery_abort (r); + return NULL; + } + } + else + { + r->do_async = GNUNET_SCHEDULER_add_now (&run_async_pc, + r); + } + return r; +} + + +void +ANASTASIS_recovery_abort (struct ANASTASIS_Recovery *r) +{ + if (NULL != r->do_async) + { + GNUNET_SCHEDULER_cancel (r->do_async); + r->do_async = NULL; + } + if (NULL != r->plo) + { + ANASTASIS_policy_lookup_cancel (r->plo); + r->plo = NULL; + } + GNUNET_free (r->solved_challenges); + for (unsigned int j = 0; j < r->ri.dps_len; j++) + GNUNET_free (r->dps[j].pub_details.challenges); + GNUNET_free (r->ri.dps); + for (unsigned int i = 0; i < r->ri.cs_len; i++) + { + struct ANASTASIS_Challenge *cs = r->ri.cs[i]; + + if (NULL != cs->kslo) + { + ANASTASIS_keyshare_lookup_cancel (cs->kslo); + cs->kslo = NULL; + } + GNUNET_free (cs->url); + GNUNET_free (cs->type); + GNUNET_free (cs->instructions); + GNUNET_free (cs->answer); + } + GNUNET_free (r->ri.cs); + GNUNET_free (r->cs); + GNUNET_free (r->dps); + json_decref (r->id_data); + GNUNET_free (r->provider_url); + GNUNET_free (r->secret_name); + GNUNET_free (r->enc_core_secret); + GNUNET_free (r); +} diff --git a/src/lib/test_merchant.priv b/src/lib/test_merchant.priv new file mode 100644 index 0000000..9c18c35 --- /dev/null +++ b/src/lib/test_merchant.priv @@ -0,0 +1 @@ +`ì&-Èí–ñ./öÀ¿ jxÌGÝ¢O:6l,ζXT4í \ No newline at end of file diff --git a/src/reducer/Makefile.am b/src/reducer/Makefile.am new file mode 100644 index 0000000..5cbe6f7 --- /dev/null +++ b/src/reducer/Makefile.am @@ -0,0 +1,45 @@ +# 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 + +.NOTPARALLEL: + +lib_LTLIBRARIES = \ + libanastasisredux.la + +libanastasisredux_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined +libanastasisredux_la_SOURCES = \ + anastasis_api_redux.c anastasis_api_redux.h \ + anastasis_api_recovery_redux.c \ + anastasis_api_backup_redux.c \ + validation_CH_AHV.c \ + validation_CZ_BN.c \ + validation_DE_SVN.c \ + validation_DE_TIN.c \ + validation_IN_AADHAR.c \ + validation_IT_CF.c \ + validation_XX_SQUARE.c \ + validation_XY_PRIME.c +libanastasisredux_la_LIBADD = \ + $(top_builddir)/src/restclient/libanastasisrest.la \ + $(top_builddir)/src/lib/libanastasis.la \ + $(top_builddir)/src/util/libanastasisutil.la \ + -lgnunetjson \ + -lgnunetcurl \ + -lgnunetutil \ + -ltalermhd \ + -ltalerutil \ + -ltalerexchange \ + -ltalermerchant \ + -ltalerjson \ + -ljansson \ + -lgcrypt \ + -ldl \ + -lm \ + $(XLIB) diff --git a/src/reducer/anastasis_api_backup_redux.c b/src/reducer/anastasis_api_backup_redux.c new file mode 100644 index 0000000..cea1360 --- /dev/null +++ b/src/reducer/anastasis_api_backup_redux.c @@ -0,0 +1,4893 @@ +/* + 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 +*/ +/** + * @file lib/anastasis_api_backup_redux.c + * @brief anastasis reducer backup api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ + +#include "platform.h" +#include "anastasis_redux.h" +#include "anastasis_api_redux.h" +#include + +/** + * How long do Anastasis providers store data if the service + * is free? Must match #ANASTASIS_MAX_YEARS_STORAGE from + * anastasis-httpd.h. + */ +#define ANASTASIS_FREE_STORAGE GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_YEARS, 5) + +/** + * CPU limiter: do not evaluate more than 16k + * possible policy combinations to find the "best" + * policy. + */ +#define MAX_EVALUATIONS (1024 * 16) + + +#define GENERATE_STRING(STRING) #STRING, +static const char *backup_strings[] = { + ANASTASIS_BACKUP_STATES (GENERATE_STRING) +}; +#undef GENERATE_STRING + + +/** + * Linked list of costs. + */ +struct Costs +{ + + /** + * Kept in a LL. + */ + struct Costs *next; + + /** + * Cost in one of the currencies. + */ + struct TALER_Amount cost; +}; + + +/** + * Add amount from @a cost to @a my_cost list. + * + * @param[in,out] my_cost pointer to list to modify + * @param cost amount to add + */ +static void +add_cost (struct Costs **my_cost, + const struct TALER_Amount *cost) +{ + for (struct Costs *pos = *my_cost; + NULL != pos; + pos = pos->next) + { + if (GNUNET_OK != + TALER_amount_cmp_currency (&pos->cost, + cost)) + continue; + GNUNET_assert (0 <= + TALER_amount_add (&pos->cost, + &pos->cost, + cost)); + return; + } + { + struct Costs *nc; + + nc = GNUNET_new (struct Costs); + nc->cost = *cost; + nc->next = *my_cost; + *my_cost = nc; + } +} + + +enum ANASTASIS_BackupState +ANASTASIS_backup_state_from_string_ (const char *state_string) +{ + for (enum ANASTASIS_BackupState i = 0; + i < sizeof (backup_strings) / sizeof(*backup_strings); + i++) + if (0 == strcmp (state_string, + backup_strings[i])) + return i; + return ANASTASIS_BACKUP_STATE_ERROR; +} + + +const char * +ANASTASIS_backup_state_to_string_ (enum ANASTASIS_BackupState bs) +{ + if ( (bs < 0) || + (bs >= sizeof (backup_strings) / sizeof(*backup_strings)) ) + { + GNUNET_break_op (0); + return NULL; + } + return backup_strings[bs]; +} + + +/** + * Update the 'backup_state' field of @a state to @a new_backup_state. + * + * @param[in,out] state the state to transition + * @param new_backup_state the state to transition to + */ +static void +set_state (json_t *state, + enum ANASTASIS_BackupState new_backup_state) +{ + GNUNET_assert ( + 0 == + json_object_set_new ( + state, + "backup_state", + json_string (ANASTASIS_backup_state_to_string_ (new_backup_state)))); +} + + +/** + * Returns an initial ANASTASIS backup state (CONTINENT_SELECTING). + * + * @param cfg handle for gnunet configuration + * @return NULL on failure + */ +json_t * +ANASTASIS_backup_start (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + json_t *initial_state; + + (void) cfg; + initial_state = ANASTASIS_REDUX_load_continents_ (); + if (NULL == initial_state) + return NULL; + set_state (initial_state, + ANASTASIS_BACKUP_STATE_CONTINENT_SELECTING); + return initial_state; +} + + +/** + * Test if @a challenge_size is small enough for the provider's + * @a size_limit_in_mb. + * + * We add 1024 to @a challenge_size here as a "safety margin" as + * the encrypted challenge has some additional headers around it + * + * @param size_limit_in_mb provider's upload limit + * @param challenge_size actual binary size of the challenge + * @return true if this fits + */ +static bool +challenge_size_ok (uint32_t size_limit_in_mb, + size_t challenge_size) +{ + return (size_limit_in_mb * 1024LLU * 1024LLU >= + challenge_size + 1024LLU); +} + + +/** + * DispatchHandler/Callback function which is called for a + * "add_authentication" action. + * Returns an #ANASTASIS_ReduxAction if operation is async. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +add_authentication (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *auth_providers; + json_t *method; + const char *method_type; + void *challenge; + size_t challenge_size; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &method_type), + GNUNET_JSON_spec_varsize ("challenge", + &challenge, + &challenge_size), + GNUNET_JSON_spec_end () + }; + + auth_providers = json_object_get (state, + "authentication_providers"); + if (NULL == auth_providers) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_providers' missing"); + return NULL; + } + + method = json_object_get (arguments, + "authentication_method"); + if (NULL == method) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'authentication_method' required"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (method, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_dumpf (method, + stderr, + JSON_INDENT (2)); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'authentication_method' content malformed"); + return NULL; + } + /* Check we know at least one provider that supports this method */ + { + bool found = false; + bool too_big = false; + json_t *details; + const char *url; + + json_object_foreach (auth_providers, url, details) + { + json_t *methods; + json_t *method; + size_t index; + uint32_t size_limit_in_mb; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", + &size_limit_in_mb), + GNUNET_JSON_spec_json ("methods", + &methods), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (details, + ispec, + NULL, NULL)) + { + GNUNET_break (0); + continue; + } + json_array_foreach (methods, index, method) + { + const char *type; + + type = json_string_value (json_object_get (method, + "type")); + GNUNET_break (NULL != type); + if ( (NULL != type) && + (0 == strcmp (type, + method_type)) ) + { + found = true; + break; + } + } + GNUNET_JSON_parse_free (ispec); + if (! challenge_size_ok (size_limit_in_mb, + challenge_size)) + { + /* Challenge data too big for this provider. Try to find another one. + Note: we add 1024 to challenge-size here as a "safety margin" as + the encrypted challenge has some additional headers around it */ + too_big = true; + found = false; + } + if (found) + break; + } + if (! found) + { + if (too_big) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_CHALLENGE_DATA_TOO_BIG, + method_type); + } + else + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_AUTHENTICATION_METHOD_NOT_SUPPORTED, + method_type); + } + GNUNET_JSON_parse_free (spec); + return NULL; + } + } + GNUNET_JSON_parse_free (spec); + + /* append provided method to our array */ + { + json_t *auth_method_arr; + + auth_method_arr = json_object_get (state, + "authentication_methods"); + if (NULL == auth_method_arr) + { + auth_method_arr = json_array (); + GNUNET_assert (0 == json_object_set_new (state, + "authentication_methods", + auth_method_arr)); + } + if (! json_is_array (auth_method_arr)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_methods' must be an array"); + return NULL; + } + GNUNET_assert (0 == + json_array_append (auth_method_arr, + method)); + cb (cb_cls, + TALER_EC_NONE, + state); + } + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "delete_authentication" action. + * Returns an #ANASTASIS_ReduxAction if operation is async. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +del_authentication (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *idx; + json_t *auth_method_arr; + + auth_method_arr = json_object_get (state, + "authentication_methods"); + if (! json_is_array (auth_method_arr)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_methods' must be an array"); + return NULL; + } + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + idx = json_object_get (arguments, + "authentication_method"); + if ( (NULL == idx) || + (! json_is_integer (idx)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'authentication_method' must be a number"); + return NULL; + } + + { + size_t index = (size_t) json_integer_value (idx); + + if (0 != json_array_remove (auth_method_arr, + index)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "removal failed"); + return NULL; + } + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/* ********************** done_authentication ******************** */ + +/** + * Which provider would be used for the given challenge, + * and at what cost? + */ +struct PolicyEntry +{ + /** + * URL of the provider. + */ + const char *provider_name; + + /** + * Recovery fee. + */ + struct TALER_Amount usage_fee; +}; + + +/** + * Map from challenges to providers. + */ +struct PolicyMap +{ + /** + * Kept in a DLL. + */ + struct PolicyMap *next; + + /** + * Kept in a DLL. + */ + struct PolicyMap *prev; + + /** + * Array of proividers selected for each challenge, + * with associated costs. + * Length of the array will be 'req_methods'. + */ + struct PolicyEntry *providers; + + /** + * Diversity score for this policy mapping. + */ + unsigned int diversity; + +}; + + +/** + * Array of challenges for a policy, and DLL with + * possible mappings of challenges to providers. + */ +struct Policy +{ + + /** + * Kept in DLL of all possible policies. + */ + struct Policy *next; + + /** + * Kept in DLL of all possible policies. + */ + struct Policy *prev; + + /** + * Head of DLL. + */ + struct PolicyMap *pm_head; + + /** + * Tail of DLL. + */ + struct PolicyMap *pm_tail; + + /** + * Challenges selected for this policy. + * Length of the array will be 'req_methods'. + */ + unsigned int *challenges; + +}; + + +/** + * Information for running done_authentication() logic. + */ +struct PolicyBuilder +{ + /** + * Authentication providers available overall, from our state. + */ + json_t *providers; + + /** + * Authentication methods available overall, from our state. + */ + const json_t *methods; + + /** + * Head of DLL of all possible policies. + */ + struct Policy *p_head; + + /** + * Tail of DLL of all possible policies. + */ + struct Policy *p_tail; + + /** + * Array of authentication policies to be computed. + */ + json_t *policies; + + /** + * Array of length @e req_methods. + */ + unsigned int *m_idx; + + /** + * Array of length @e req_methods identifying a set of providers selected + * for each authentication method, while we are trying to compute the + * 'best' allocation of providers to authentication methods. + * Only valid during the go_with() function. + */ + const char **best_sel; + + /** + * Error hint to return on failure. Set if @e ec is not #TALER_EC_NONE. + */ + const char *hint; + + /** + * Policy we are currently building maps for. + */ + struct Policy *current_policy; + + /** + * LL of costs associated with the currently preferred + * policy. + */ + struct Costs *best_cost; + + /** + * Array of 'best' policy maps found so far, + * ordered by policy. + */ + struct PolicyMap *best_map; + + /** + * Array of the currency policy maps under evaluation + * by find_best_map(). + */ + struct PolicyMap *curr_map; + + /** + * How many mappings have we evaluated so far? + * Used to limit the computation by aborting after + * #MAX_EVALUATIONS trials. + */ + unsigned int evaluations; + + /** + * Overall number of challenges provided by the user. + */ + unsigned int num_methods; + + /** + * Number of challenges that must be satisfied to recover the secret. + * Derived from the total number of challenges entered by the user. + */ + unsigned int req_methods; + + /** + * Number of different Anastasis providers selected in @e best_sel. + * Only valid during the go_with() function. + */ + unsigned int best_diversity; + + /** + * Number of identical challenges duplicated at + * various providers in the best case. Smaller is + * better. + */ + unsigned int best_duplicates; + + /** + * Error code to return, #TALER_EC_NONE on success. + */ + enum TALER_ErrorCode ec; + +}; + + +/** + * Free @a costs LL. + * + * @param[in] costs linked list to free + */ +static void +free_costs (struct Costs *costs) +{ + if (NULL == costs) + return; + free_costs (costs->next); + GNUNET_free (costs); +} + + +/** + * Check if providers @a p1 and @a p2 have equivalent + * methods and cost structures. + * + * @return true if the providers are fully equivalent + */ +static bool +equiv_provider (struct PolicyBuilder *pb, + const char *p1, + const char *p2) +{ + json_t *j1; + json_t *j2; + json_t *m1; + json_t *m2; + struct TALER_Amount uc1; + struct TALER_Amount uc2; + + j1 = json_object_get (pb->providers, + p1); + j2 = json_object_get (pb->providers, + p2); + if ( (NULL == j1) || + (NULL == j2) ) + { + GNUNET_break (0); + return false; + } + + { + struct GNUNET_JSON_Specification s1[] = { + GNUNET_JSON_spec_json ("methods", + &m1), + TALER_JSON_spec_amount_any ("truth_upload_fee", + &uc1), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j1, + s1, + NULL, NULL)) + { + GNUNET_break (0); + return false; + } + } + + { + struct GNUNET_JSON_Specification s2[] = { + GNUNET_JSON_spec_json ("methods", + &m2), + TALER_JSON_spec_amount_any ("truth_upload_fee", + &uc2), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j2, + s2, + NULL, NULL)) + { + GNUNET_break (0); + return false; + } + } + + if ( (GNUNET_OK != + TALER_amount_cmp_currency (&uc1, + &uc2)) || + (0 != + TALER_amount_cmp (&uc1, + &uc2)) ) + return false; + + if (json_array_size (m1) != json_array_size (m2)) + return false; + { + size_t idx1; + json_t *e1; + + json_array_foreach (m1, idx1, e1) + { + const char *type1; + struct TALER_Amount fee1; + struct GNUNET_JSON_Specification s1[] = { + GNUNET_JSON_spec_string ("type", + &type1), + TALER_JSON_spec_amount_any ("usage_fee", + &fee1), + GNUNET_JSON_spec_end () + }; + bool matched = false; + + if (GNUNET_OK != + GNUNET_JSON_parse (e1, + s1, + NULL, NULL)) + { + GNUNET_break (0); + return false; + } + { + size_t idx2; + json_t *e2; + + json_array_foreach (m2, idx2, e2) + { + const char *type2; + struct TALER_Amount fee2; + struct GNUNET_JSON_Specification s2[] = { + GNUNET_JSON_spec_string ("type", + &type2), + TALER_JSON_spec_amount_any ("usage_fee", + &fee2), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (e2, + s2, + NULL, NULL)) + { + GNUNET_break (0); + return false; + } + if ( (0 == strcmp (type1, + type2)) && + (GNUNET_OK == + TALER_amount_cmp_currency (&fee1, + &fee2)) && + (0 == TALER_amount_cmp (&fee1, + &fee2)) ) + { + matched = true; + break; + } + } + } + if (! matched) + return false; + } + } + return true; +} + + +/** + * Evaluate the cost/benefit of the provider selection in @a prov_sel + * and if it is better then the best known one in @a pb, update @a pb. + * + * @param[in,out] pb our operational context + * @param[in,out] prov_sel array of req_methods provider indices to complete + */ +static void +eval_provider_selection (struct PolicyBuilder *pb, + const char *prov_sel[]) +{ + unsigned int curr_diversity; + struct PolicyEntry policy_ent[pb->req_methods]; + + for (unsigned int i = 0; i < pb->req_methods; i++) + { + const json_t *method_obj = json_array_get (pb->methods, + pb->m_idx[i]); + const json_t *provider_cfg = json_object_get (pb->providers, + prov_sel[i]); + json_t *provider_methods; + const char *method_type; + json_t *md; + size_t index; + bool found = false; + uint32_t size_limit_in_mb; + struct TALER_Amount upload_cost; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", + &size_limit_in_mb), + GNUNET_JSON_spec_json ("methods", + &provider_methods), + TALER_JSON_spec_amount_any ("truth_upload_fee", + &upload_cost), + GNUNET_JSON_spec_end () + }; + void *challenge; + size_t challenge_size; + struct GNUNET_JSON_Specification mspec[] = { + GNUNET_JSON_spec_string ("type", + &method_type), + GNUNET_JSON_spec_varsize ("challenge", + &challenge, + &challenge_size), + GNUNET_JSON_spec_end () + }; + + policy_ent[i].provider_name = prov_sel[i]; + if (GNUNET_OK != + GNUNET_JSON_parse (method_obj, + mspec, + NULL, NULL)) + { + GNUNET_break (0); + pb->ec = TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID; + pb->hint = "'authentication_method' content malformed"; + return; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (provider_cfg, + pspec, + NULL, NULL)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Skipping provider %s: no suitable configuration\n", + prov_sel[i]); + GNUNET_JSON_parse_free (mspec); + return; + } + json_array_foreach (provider_methods, index, md) + { + const char *type; + struct TALER_Amount method_cost; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + TALER_JSON_spec_amount_any ("usage_fee", + &method_cost), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (md, + spec, + NULL, NULL)) + { + GNUNET_break (0); + pb->ec = TALER_EC_ANASTASIS_REDUCER_STATE_INVALID; + pb->hint = "'methods' of provider"; + GNUNET_JSON_parse_free (pspec); + return; + } + if ( (0 == strcmp (type, + method_type)) && + (challenge_size_ok (size_limit_in_mb, + challenge_size) ) ) + { + found = true; + GNUNET_break (0 <= + TALER_amount_add (&policy_ent[i].usage_fee, + &method_cost, + &upload_cost)); + } + } + if (! found) + { + /* Provider does not OFFER this method, combination not possible. + Cost is basically 'infinite', but we simply then skip this. */ + GNUNET_JSON_parse_free (pspec); + GNUNET_JSON_parse_free (mspec); + return; + } + GNUNET_JSON_parse_free (mspec); + GNUNET_JSON_parse_free (pspec); + } + + /* calculate provider diversity by counting number of different + providers selected */ + curr_diversity = 0; + for (unsigned int i = 0; i < pb->req_methods; i++) + { + bool found = false; + + for (unsigned int j = 0; j < i; j++) + { + if (prov_sel[i] == prov_sel[j]) + { + found = true; + break; + } + } + if (! found) + curr_diversity++; + } +#if DEBUG + fprintf (stderr, + "Diversity: %u (best: %u)\n", + curr_diversity, + pb->best_diversity); +#endif + if (curr_diversity < pb->best_diversity) + return; /* do not allow combinations that are bad + for provider diversity */ + if (curr_diversity > pb->best_diversity) + { + /* drop existing policies, they are all worse */ + struct PolicyMap *m; + + while (NULL != (m = pb->current_policy->pm_head)) + { + GNUNET_CONTAINER_DLL_remove (pb->current_policy->pm_head, + pb->current_policy->pm_tail, + m); + GNUNET_free (m->providers); + GNUNET_free (m); + } + pb->best_diversity = curr_diversity; + } + if (NULL == pb->p_head) + { + /* For the first policy, check for equivalent + policy mapping existing: we + do not want to do spend CPU time investigating + purely equivalent permutations */ + for (struct PolicyMap *m = pb->current_policy->pm_head; + NULL != m; + m = m->next) + { + bool equiv = true; + for (unsigned int i = 0; ireq_methods; i++) + { + if (! equiv_provider (pb, + m->providers[i].provider_name, + policy_ent[i].provider_name)) + { + equiv = false; + break; + } + } + if (equiv) + return; /* equivalent to known allocation */ + } + } + + /* Add possible mapping to result list */ + { + struct PolicyMap *m; + + m = GNUNET_new (struct PolicyMap); + m->providers = GNUNET_new_array (pb->req_methods, + struct PolicyEntry); + memcpy (m->providers, + policy_ent, + sizeof (struct PolicyEntry) * pb->req_methods); + m->diversity = curr_diversity; + GNUNET_CONTAINER_DLL_insert (pb->current_policy->pm_head, + pb->current_policy->pm_tail, + m); + } +} + + +/** + * Recursively compute possible combination(s) of provider candidates + * in @e prov_sel. The selection is complete up to index @a i. Calls + * eval_provider_selection() upon a feasible provider selection for + * evaluation, resulting in "better" combinations being persisted in + * @a pb. + * + * @param[in,out] pb our operational context + * @param[in,out] prov_sel array of req_methods provider URLs to complete + * @param i index up to which @a prov_sel is already initialized + */ +static void +provider_candidate (struct PolicyBuilder *pb, + const char *prov_sel[], + unsigned int i) +{ + const char *url; + json_t *pconfig; + + json_object_foreach (pb->providers, url, pconfig) + { + prov_sel[i] = url; + if (i == pb->req_methods - 1) + { + eval_provider_selection (pb, + prov_sel); + if (TALER_EC_NONE != pb->ec) + break; + continue; + } + provider_candidate (pb, + prov_sel, + i + 1); + } +} + + +/** + * Using the selection of authentication methods from @a pb in + * "m_idx", compute the best choice of providers. + * + * @param[in,out] pb our operational context + */ +static void +go_with (struct PolicyBuilder *pb) +{ + const char *prov_sel[pb->req_methods]; + struct Policy *policy; + + /* compute provider selection */ + policy = GNUNET_new (struct Policy); + policy->challenges = GNUNET_new_array (pb->req_methods, + unsigned int); + memcpy (policy->challenges, + pb->m_idx, + pb->req_methods * sizeof (unsigned int)); + pb->current_policy = policy; + pb->best_diversity = 0; + provider_candidate (pb, + prov_sel, + 0); + GNUNET_CONTAINER_DLL_insert (pb->p_head, + pb->p_tail, + policy); + pb->current_policy = NULL; +} + + +/** + * Recursively computes all possible subsets of length "req_methods" + * from an array of length "num_methods", calling "go_with" on each of + * those subsets (in "m_idx"). + * + * @param[in,out] pb our operational context + * @param i offset up to which the "m_idx" has been computed + */ +static void +method_candidate (struct PolicyBuilder *pb, + unsigned int i) +{ + unsigned int start; + unsigned int *m_idx = pb->m_idx; + + start = (i > 0) ? m_idx[i - 1] + 1 : 0; + for (unsigned int j = start; j < pb->num_methods; j++) + { + m_idx[i] = j; + if (i == pb->req_methods - 1) + { +#if DEBUG + fprintf (stderr, + "Suggesting: "); + for (unsigned int k = 0; kreq_methods; k++) + { + fprintf (stderr, + "%u ", + m_idx[k]); + } + fprintf (stderr, "\n"); +#endif + go_with (pb); + continue; + } + method_candidate (pb, + i + 1); + } +} + + +/** + * Lookup @a salt of @a provider_url in @a state. + * + * @param state the state to inspect + * @param provider_url provider to look into + * @param[out] salt value to extract + * @return #GNUNET_OK on success + */ +static int +lookup_salt (const json_t *state, + const char *provider_url, + struct ANASTASIS_CRYPTO_ProviderSaltP *salt) +{ + const json_t *aps; + const json_t *cfg; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("salt", + salt), + GNUNET_JSON_spec_end () + }; + + aps = json_object_get (state, + "authentication_providers"); + if (NULL == aps) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + cfg = json_object_get (aps, + provider_url); + if (NULL == cfg) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_JSON_parse (cfg, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Compare two cost lists. + * + * @param my cost to compare + * @param be cost to compare + * @return 0 if costs are estimated equal, + * 1 if @a my < @a be + * -1 if @a my > @a be + */ +static int +compare_costs (const struct Costs *my, + const struct Costs *be) +{ + int ranking = 0; + + for (const struct Costs *cmp = be; + NULL != cmp; + cmp = cmp->next) + { + bool found = false; + + for (const struct Costs *pos = my; + NULL != pos; + pos = pos->next) + { + if (GNUNET_OK != + TALER_amount_cmp_currency (&cmp->cost, + &pos->cost)) + continue; + found = true; + } + if (! found) + ranking--; /* new policy has no cost in this currency */ + } + + for (const struct Costs *pos = my; + NULL != pos; + pos = pos->next) + { + bool found = false; + + for (const struct Costs *cmp = be; + NULL != cmp; + cmp = cmp->next) + { + if (GNUNET_OK != + TALER_amount_cmp_currency (&cmp->cost, + &pos->cost)) + continue; + found = true; + switch (TALER_amount_cmp (&cmp->cost, + &pos->cost)) + { + case -1: /* cmp < pos */ + ranking--; + break; + case 0: + break; + case 1: /* cmp > pos */ + ranking++; + break; + } + break; + } + if (! found) + ranking++; /* old policy has no cost in this currency */ + } + if (0 == ranking) + return 0; + return (0 > ranking) ? -1 : 1; +} + + +/** + * Evaluate the combined policy map stack in the ``curr_map`` of @a pb + * and compare to the current best cost. If we are better, save the + * stack in the ``best_map``. + * + * @param[in,out] pb policy builder we evaluate for + * @param num_policies length of the ``curr_map`` array + */ +static void +evaluate_map (struct PolicyBuilder *pb, + unsigned int num_policies) +{ + struct Costs *my_cost = NULL; + unsigned int i = 0; + unsigned int duplicates = 0; + int ccmp; + +#if DEBUG + fprintf (stderr, + "Checking...\n"); +#endif + /* calculate cost */ + for (const struct Policy *p = pb->p_head; + NULL != p; + p = p->next) + { + const struct PolicyMap *pm = &pb->curr_map[i++]; + +#if DEBUG + fprintf (stderr, + "Evaluating %p (%u): ", + p, + pm->diversity); + for (unsigned int k = 0; kreq_methods; k++) + { + const struct PolicyEntry *pe = &pm->providers[k]; + + fprintf (stderr, + "%u->%s ", + p->challenges[k], + pe->provider_name); + } + fprintf (stderr, "\n"); +#endif + for (unsigned int j = 0; jreq_methods; j++) + { + const struct PolicyEntry *pe = &pm->providers[j]; + unsigned int cv = p->challenges[j]; + bool found = false; + unsigned int i2 = 0; + + /* check for duplicates */ + for (const struct Policy *p2 = pb->p_head; + p2 != p; + p2 = p2->next) + { + const struct PolicyMap *pm2 = &pb->curr_map[i2++]; + + for (unsigned int j2 = 0; j2req_methods; j2++) + { + const struct PolicyEntry *pe2 = &pm2->providers[j2]; + unsigned int cv2 = p2->challenges[j2]; + + if (cv != cv2) + continue; /* different challenge */ + if (0 == strcmp (pe->provider_name, + pe2->provider_name)) + found = true; /* same challenge&provider! */ + else + duplicates++; /* penalty for same challenge at two providers */ + } + } + if (found) + continue; /* cost already included, do not add */ + add_cost (&my_cost, + &pe->usage_fee); + } + } + + ccmp = -1; /* non-zero if 'best_duplicates' is UINT_MAX */ + if ( (UINT_MAX != pb->best_duplicates) && + (0 > (ccmp = compare_costs (my_cost, + pb->best_cost))) ) + { + /* new method not clearly better, do not use it */ + free_costs (my_cost); +#if DEBUG + fprintf (stderr, + "... useless\n"); +#endif + return; + } + if ( (0 == ccmp) && + (duplicates > pb->best_duplicates) ) + { + /* new method is cost-equal, but looses on duplicates, + do not use it */ + free_costs (my_cost); +#if DEBUG + fprintf (stderr, + "... useless\n"); +#endif + return; + } + /* new method is better (or first), set as best */ +#if DEBUG + fprintf (stderr, + "New best: %u duplicates, %s cost\n", + duplicates, + TALER_amount2s (&my_cost->cost)); +#endif + free_costs (pb->best_cost); + pb->best_cost = my_cost; + pb->best_duplicates = duplicates; + memcpy (pb->best_map, + pb->curr_map, + sizeof (struct PolicyMap) * num_policies); +} + + +/** + * Try all policy maps for @a pos and evaluate the + * resulting total cost, saving the best result in + * @a pb. + * + * @param[in,out] pb policy builder context + * @param pos policy we are currently looking at maps for + * @param off index of @a pos for the policy map + */ +static void +find_best_map (struct PolicyBuilder *pb, + struct Policy *pos, + unsigned int off) +{ + if (NULL == pos) + { + evaluate_map (pb, + off); + pb->evaluations++; + return; + } + for (struct PolicyMap *pm = pos->pm_head; + NULL != pm; + pm = pm->next) + { + pb->curr_map[off] = *pm; + find_best_map (pb, + pos->next, + off + 1); + if (pb->evaluations >= MAX_EVALUATIONS) + break; + } +} + + +/** + * Select cheapest policy combinations and add them to the JSON ``policies`` + * array in @a pb + * + * @param[in,out] policy builder with our state + */ +static void +select_policies (struct PolicyBuilder *pb) +{ + unsigned int cnt = 0; + + for (struct Policy *p = pb->p_head; + NULL != p; + p = p->next) + cnt++; + { + struct PolicyMap best[cnt]; + struct PolicyMap curr[cnt]; + unsigned int i; + + pb->best_map = best; + pb->curr_map = curr; + pb->best_duplicates = UINT_MAX; /* worst */ + find_best_map (pb, + pb->p_head, + 0); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Assessed %u/%u policies\n", + pb->evaluations, + (unsigned int) MAX_EVALUATIONS); + i = 0; + for (struct Policy *p = pb->p_head; + NULL != p; + p = p->next) + { + struct PolicyMap *pm = &best[i++]; + json_t *method_arr; + +#if DEBUG + fprintf (stderr, + "Best map (%u): ", + pm->diversity); + for (unsigned int k = 0; kreq_methods; k++) + { + fprintf (stderr, + "%u->%s ", + p->challenges[k], + pm->providers[k].provider_name); + } + fprintf (stderr, "\n"); +#endif + /* Convert "best" selection into 'policies' array */ + method_arr = json_array (); + GNUNET_assert (NULL != method_arr); + for (unsigned int i = 0; i < pb->req_methods; i++) + { + json_t *policy_method = json_pack ("{s:I, s:s}", + "authentication_method", + (json_int_t) p->challenges[i], + "provider", + pm->providers[i].provider_name); + GNUNET_assert (0 == + json_array_append_new (method_arr, + policy_method)); + } + { + json_t *policy = json_pack ("{s:o}", + "methods", + method_arr); + GNUNET_assert (NULL != policy); + GNUNET_assert (0 == + json_array_append_new (pb->policies, + policy)); + } + } + } +} + + +/** + * Clean up @a pb, in particular the policies DLL. + * + * @param[in] pb builder to clean up + */ +static void +clean_pb (struct PolicyBuilder *pb) +{ + struct Policy *p; + + while (NULL != (p = pb->p_head)) + { + struct PolicyMap *pm; + + while (NULL != (pm = p->pm_head)) + { + GNUNET_CONTAINER_DLL_remove (p->pm_head, + p->pm_tail, + pm); + GNUNET_free (pm->providers); + GNUNET_free (pm); + } + GNUNET_CONTAINER_DLL_remove (pb->p_head, + pb->p_tail, + p); + GNUNET_free (p->challenges); + GNUNET_free (p); + } +} + + +/** + * DispatchHandler/Callback function which is called for a + * "done_authentication" action. Automaticially computes policies + * based on available Anastasis providers and challenges provided by + * the user. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +done_authentication (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct PolicyBuilder pb = { + .ec = TALER_EC_NONE + }; + json_t *providers; + json_t *policy_providers; + + pb.providers = json_object_get (state, + "authentication_providers"); + if ( (NULL == pb.providers) || + (! json_is_object (pb.providers) ) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_providers' must be provided"); + return NULL; + } + pb.methods = json_object_get (state, + "authentication_methods"); + if ( (NULL == pb.methods) || + (! json_is_array (pb.methods)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_methods' must be provided"); + return NULL; + } + pb.num_methods = json_array_size (pb.methods); + switch (pb.num_methods) + { + case 0: + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_methods' must not be empty"); + return NULL; + case 1: + case 2: + pb.req_methods = pb.num_methods; + break; + case 3: + case 4: + pb.req_methods = pb.num_methods - 1; + break; + case 5: + case 6: + pb.req_methods = pb.num_methods - 2; + break; + case 7: + pb.req_methods = pb.num_methods - 3; + break; + default: + /* cap at 4 for auto-generation, algorithm + to compute mapping gets too expensive + otherwise. */ + pb.req_methods = 4; + break; + } + { + unsigned int m_idx[pb.req_methods]; + + /* select req_methods from num_methods. */ + pb.m_idx = m_idx; + method_candidate (&pb, + 0); + } + pb.policies = json_array (); + select_policies (&pb); + clean_pb (&pb); + if (TALER_EC_NONE != pb.ec) + { + json_decref (pb.policies); + ANASTASIS_redux_fail_ (cb, + cb_cls, + pb.ec, + pb.hint); + return NULL; + } + GNUNET_assert (0 == + json_object_set_new (state, + "policies", + pb.policies)); + providers = json_object_get (arguments, + "providers"); + if (NULL == providers) + { + /* Setup a providers array from all working providers */ + json_t *available = json_object_get (state, + "authentication_providers"); + const char *url; + json_t *details; + + policy_providers = json_array (); + json_object_foreach (available, url, details) + { + json_t *provider; + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + + if (GNUNET_OK != + lookup_salt (state, + url, + &salt)) + continue; /* skip providers that are down */ + provider = json_pack ("{s:s}", + "provider_url", url); + GNUNET_assert (NULL != provider); + GNUNET_assert (0 == + json_array_append_new (policy_providers, + provider)); + } + } + else + { + /* Setup a providers array from all working providers */ + size_t off; + json_t *url; + + policy_providers = json_array (); + json_array_foreach (providers, off, url) + { + json_t *provider; + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + const char *url_str; + + url_str = json_string_value (url); + if ( (NULL == url_str) || + (GNUNET_OK != + lookup_salt (state, + url_str, + &salt)) ) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "unworkable provider requested"); + return NULL; + } + provider = json_pack ("{s:s}", + "provider_url", url); + GNUNET_assert (NULL != provider); + GNUNET_assert (0 == + json_array_append_new (policy_providers, + provider)); + } + } + if (0 == json_array_size (policy_providers)) + { + json_decref (policy_providers); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "no workable providers in state"); + return NULL; + } + GNUNET_assert (0 == + json_object_set_new (state, + "policy_providers", + policy_providers)); + set_state (state, + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/* ******************** add_provider ******************* */ + + +/** + * DispatchHandler/Callback function which is called for a + * "add_provider" action. Adds another Anastasis provider + * to the list of available providers for storing information. + * + * @param state state to operate on + * @param arguments arguments with a provider URL to add + * @param cb callback to call during/after operation + * @param cb_cls callback closure + */ +static struct ANASTASIS_ReduxAction * +add_provider (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + if (ANASTASIS_add_provider_ (state, + arguments, + cb, + cb_cls)) + return NULL; + return ANASTASIS_REDUX_backup_begin_ (state, + NULL, + cb, + cb_cls); +} + + +/* ******************** add_policy ******************* */ + + +/** + * DispatchHandler/Callback function which is called for a + * "add_policy" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +add_policy (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *arg_array; + json_t *policies; + const json_t *auth_providers; + const json_t *auth_methods; + json_t *methods; + + if (NULL == arguments) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + arg_array = json_object_get (arguments, + "policy"); + if (! json_is_array (arg_array)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'policy' not an array"); + return NULL; + } + policies = json_object_get (state, + "policies"); + if (! json_is_array (policies)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' not an array"); + return NULL; + } + auth_providers = json_object_get (state, + "authentication_providers"); + if (! json_is_object (auth_providers)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'auth_providers' not an object"); + return NULL; + } + auth_methods = json_object_get (state, + "authentication_methods"); + if (! json_is_array (auth_methods)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'auth_methods' not an array"); + return NULL; + } + + methods = json_array (); + GNUNET_assert (NULL != methods); + + /* Add all methods from 'arg_array' to 'methods' */ + { + size_t index; + json_t *method; + + json_array_foreach (arg_array, index, method) + { + const char *provider_url; + uint32_t method_idx; + json_t *prov_methods; + const char *method_type; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("provider", + &provider_url), + GNUNET_JSON_spec_uint32 ("authentication_method", + &method_idx), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (method, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'method' details malformed"); + return NULL; + } + + { + const json_t *prov_cfg; + uint32_t limit; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", + &limit), + GNUNET_JSON_spec_json ("methods", + &prov_methods), + GNUNET_JSON_spec_end () + }; + + prov_cfg = json_object_get (auth_providers, + provider_url); + if (NULL == prov_cfg) + { + GNUNET_break (0); + json_decref (methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "provider URL unknown"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (prov_cfg, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "provider lacks authentication methods"); + return NULL; + + } + if (! json_is_array (prov_methods)) + { + GNUNET_break (0); + json_decref (methods); + json_decref (prov_methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "provider lacks authentication methods"); + return NULL; + } + } + + { + const json_t *auth_method; + + auth_method = json_array_get (auth_methods, + method_idx); + if (NULL == auth_method) + { + GNUNET_break (0); + json_decref (methods); + json_decref (prov_methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "authentication method unknown"); + return NULL; + } + method_type = json_string_value (json_object_get (auth_method, + "type")); + if (NULL == method_type) + { + GNUNET_break (0); + json_decref (methods); + json_decref (prov_methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "authentication method must be a string"); + return NULL; + } + } + + { + bool found = false; + size_t index; + json_t *pm; + json_array_foreach (prov_methods, index, pm) + { + struct TALER_Amount method_cost; + const char *type; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + TALER_JSON_spec_amount_any ("usage_fee", + &method_cost), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (pm, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (methods); + json_decref (prov_methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "provider authentication method specification invalid"); + return NULL; + } + if (0 != strcmp (type, + method_type)) + continue; + found = true; + break; + } + if (! found) + { + GNUNET_break (0); + json_decref (methods); + json_decref (prov_methods); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "selected provider does not support authentication method"); + return NULL; + } + } + GNUNET_assert (0 == + json_array_append (methods, + method)); + json_decref (prov_methods); + } /* end of json_array_foreach (arg_array, index, method) */ + } + + /* add new policy to array of existing policies */ + { + json_t *policy; + json_t *idx; + + policy = json_pack ("{s:o}", + "methods", + methods); + GNUNET_assert (NULL != policy); + idx = json_object_get (arguments, + "policy_index"); + if ( (NULL == idx) || + (! json_is_integer (idx)) ) + { + GNUNET_assert (0 == + json_array_append_new (policies, + policy)); + } + else + { + GNUNET_assert (0 == + json_array_insert_new (policies, + json_integer_value (idx), + policy)); + } + } + + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/* ******************** update_policy ******************* */ + + +/** + * DispatchHandler/Callback function which is called for a + * "update_policy" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +update_policy (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *idx; + size_t index; + json_t *policy_arr; + + if (NULL == arguments) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + idx = json_object_get (arguments, + "policy_index"); + if (! json_is_integer (idx)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'policy_index' must be an integer"); + return NULL; + } + index = json_integer_value (idx); + policy_arr = json_object_get (state, + "policies"); + if (! json_is_array (policy_arr)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' must be an array"); + return NULL; + } + if (0 != json_array_remove (policy_arr, + index)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "removal failed"); + return NULL; + } + return add_policy (state, + arguments, + cb, + cb_cls); +} + + +/* ******************** del_policy ******************* */ + + +/** + * DispatchHandler/Callback function which is called for a + * "delete_policy" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +del_policy (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *idx; + size_t index; + json_t *policy_arr; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + idx = json_object_get (arguments, + "policy_index"); + if (! json_is_integer (idx)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'policy_index' must be an integer"); + return NULL; + } + index = json_integer_value (idx); + policy_arr = json_object_get (state, + "policies"); + if (! json_is_array (policy_arr)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' must be an array"); + return NULL; + } + if (0 != json_array_remove (policy_arr, + index)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "removal failed"); + return NULL; + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/* ******************** del_challenge ******************* */ + + +/** + * DispatchHandler/Callback function which is called for a + * "delete_challenge" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +del_challenge (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *pidx; + const json_t *cidx; + size_t index; + json_t *policy_arr; + json_t *policy; + json_t *method_arr; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + pidx = json_object_get (arguments, + "policy_index"); + cidx = json_object_get (arguments, + "challenge_index"); + if (! json_is_integer (pidx)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'policy_index' must be an integer"); + return NULL; + } + if (! json_is_integer (cidx)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'challenge_index' must be an integer"); + return NULL; + } + index = json_integer_value (pidx); + policy_arr = json_object_get (state, + "policies"); + if (! json_is_array (policy_arr)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' must be an array"); + return NULL; + } + policy = json_array_get (policy_arr, + index); + if (NULL == policy) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'policy_index' out of range"); + return NULL; + } + method_arr = json_object_get (policy, + "methods"); + if (NULL == method_arr) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "methods missing in policy"); + return NULL; + } + index = json_integer_value (cidx); + if (0 != json_array_remove (method_arr, + index)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "removal failed"); + return NULL; + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/* ********************** done_policy_review ***************** */ + + +/** + * Calculate how many years of service we need + * from the desired @a expiration time, + * rounding up. + * + * @param expiration desired expiration time + * @return number of years of service to pay for +*/ +static unsigned int +expiration_to_years (struct GNUNET_TIME_Absolute expiration) +{ + struct GNUNET_TIME_Relative rem; + unsigned int years; + + rem = GNUNET_TIME_absolute_get_remaining (expiration); + years = rem.rel_value_us / GNUNET_TIME_UNIT_YEARS.rel_value_us; + if (0 != rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us) + years++; + return years; +} + + +/** + * Update @a state such that the earliest expiration for + * any truth or policy is @a expiration. Recalculate + * the ``upload_fees`` array with the associated costs. + * + * @param[in,out] state our state to update + * @param expiration new expiration to enforce + * @return #GNUNET_OK on success, + * #GNUNET_SYSERR if the state is invalid + */ +static enum GNUNET_GenericReturnValue +update_expiration_cost (json_t *state, + struct GNUNET_TIME_Absolute expiration) +{ + struct Costs *costs = NULL; + unsigned int years; + json_t *providers; + bool is_free = true; + + providers = json_object_get (state, + "authentication_providers"); + if (! json_is_object (providers)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + years = expiration_to_years (expiration); + + /* go over all providers and add up cost */ + { + const char *url; + json_t *provider; + + json_object_foreach (providers, url, provider) + { + struct TALER_Amount annual_fee; + struct GNUNET_JSON_Specification pspec[] = { + TALER_JSON_spec_amount_any ("annual_fee", + &annual_fee), + GNUNET_JSON_spec_end () + }; + struct TALER_Amount fee; + + if (GNUNET_OK != + GNUNET_JSON_parse (provider, + pspec, + NULL, NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_multiply (&fee, + &annual_fee, + years)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + add_cost (&costs, + &fee); + } + } + + /* go over all truths and add up cost */ + { + unsigned int off = 0; + unsigned int len = 0; + struct AlreadySeen + { + uint32_t method; + const char *provider_url; + } *seen = NULL; + json_t *policies; + size_t pidx; + json_t *policy; + + policies = json_object_get (state, + "policies"); + json_array_foreach (policies, pidx, policy) + { + json_t *methods; + json_t *method; + size_t midx; + + methods = json_object_get (policy, + "methods"); + json_array_foreach (methods, midx, method) + { + const char *provider_url; + uint32_t method_idx; + + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("provider", + &provider_url), + GNUNET_JSON_spec_uint32 ("authentication_method", + &method_idx), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (method, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + /* check if we have seen this one before */ + { + bool found = false; + + for (unsigned int i = 0; i + TALER_amount_multiply (&fee, + &upload_cost, + years)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + add_cost (&costs, + &fee); + } + } + } + GNUNET_array_grow (seen, + len, + 0); + } + + /* convert 'costs' into state */ + { + json_t *arr; + + arr = json_array (); + GNUNET_assert (NULL != arr); + while (NULL != costs) + { + struct Costs *nxt = costs->next; + + if ( (0 != costs->cost.value) || + (0 != costs->cost.fraction) ) + { + json_t *ao; + + ao = json_pack ("{s:o}", + "fee", + TALER_JSON_from_amount (&costs->cost)); + GNUNET_assert (0 == + json_array_append_new (arr, + ao)); + is_free = false; + } + GNUNET_free (costs); + costs = nxt; + } + GNUNET_assert (0 == + json_object_set_new (state, + "upload_fees", + arr)); + } + + if (is_free) + expiration = GNUNET_TIME_relative_to_absolute (ANASTASIS_FREE_STORAGE); + /* update 'expiration' in state */ + { + json_t *eo; + + (void) GNUNET_TIME_round_abs (&expiration); + eo = GNUNET_JSON_from_time_abs (expiration); + GNUNET_assert (0 == + json_object_set_new (state, + "expiration", + eo)); + } + + + return GNUNET_OK; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "done_policy_review" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +done_policy_review (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *policy_arr; + + policy_arr = json_object_get (state, + "policies"); + if (0 == json_array_size (policy_arr)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "no policies specified"); + return NULL; + } + { + struct GNUNET_TIME_Absolute exp = {0}; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_absolute_time ("expiration", + &exp)), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "invalid expiration specified"); + return NULL; + } + if (0 == exp.abs_value_us) + exp = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_YEARS); + if (GNUNET_OK != + update_expiration_cost (state, + exp)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "could not calculate expiration cost"); + return NULL; + } + } + set_state (state, + ANASTASIS_BACKUP_STATE_SECRET_EDITING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * Information we keep for an upload() operation. + */ +struct UploadContext; + + +/** + * Maps a TruthUpload to a policy and recovery method where this + * truth is used. + */ +struct PolicyMethodReference +{ + /** + * Offset into the "policies" array. + */ + unsigned int policy_index; + + /** + * Offset into the "methods" array (of the policy selected + * by @e policy_index). + */ + unsigned int method_index; + +}; + + +/** + * Entry we keep per truth upload. + */ +struct TruthUpload +{ + + /** + * Kept in a DLL. + */ + struct TruthUpload *next; + + /** + * Kept in a DLL. + */ + struct TruthUpload *prev; + + /** + * Handle to the actual upload operation. + */ + struct ANASTASIS_TruthUpload *tu; + + /** + * Upload context this operation is part of. + */ + struct UploadContext *uc; + + /** + * Truth resulting from the upload, if any. + */ + struct ANASTASIS_Truth *t; + + /** + * A taler://pay/-URI with a request to pay the annual fee for + * the service. Set if payment is required. + */ + char *payment_request; + + /** + * Which policies and methods does this truth affect? + */ + struct PolicyMethodReference *policies; + + /** + * Where are we uploading to? + */ + char *provider_url; + + /** + * Which challenge object are we uploading? + */ + uint32_t am_idx; + + /** + * Length of the @e policies array. + */ + unsigned int policies_length; + + /** + * Status of the upload. + */ + enum ANASTASIS_UploadStatus us; + + /** + * Taler error code of the upload. + */ + enum TALER_ErrorCode ec; + +}; + + +/** + * Information we keep for an upload() operation. + */ +struct UploadContext +{ + /** + * Recovery action returned to caller for aborting the operation. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * Function to call upon completion. + */ + ANASTASIS_ActionCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Our state. + */ + json_t *state; + + /** + * Master secret sharing operation, NULL if not yet running. + */ + struct ANASTASIS_SecretShare *ss; + + /** + * Head of DLL of truth uploads. + */ + struct TruthUpload *tues_head; + + /** + * Tail of DLL of truth uploads. + */ + struct TruthUpload *tues_tail; + + /** + * Timeout to use for the operation, from the arguments. + */ + struct GNUNET_TIME_Relative timeout; + + /** + * For how many years should we pay? + */ + unsigned int years; + +}; + + +/** + * Function called when the #upload transition is being aborted. + * + * @param cls a `struct UploadContext` + */ +static void +upload_cancel_cb (void *cls) +{ + struct UploadContext *uc = cls; + struct TruthUpload *tue; + + while (NULL != (tue = uc->tues_head)) + { + GNUNET_CONTAINER_DLL_remove (uc->tues_head, + uc->tues_tail, + tue); + if (NULL != tue->tu) + { + ANASTASIS_truth_upload_cancel (tue->tu); + tue->tu = NULL; + } + if (NULL != tue->t) + { + ANASTASIS_truth_free (tue->t); + tue->t = NULL; + } + GNUNET_free (tue->provider_url); + GNUNET_free (tue->payment_request); + GNUNET_free (tue->policies); + GNUNET_free (tue); + } + if (NULL != uc->ss) + { + ANASTASIS_secret_share_cancel (uc->ss); + uc->ss = NULL; + } + json_decref (uc->state); + GNUNET_free (uc); +} + + +/** + * Take all of the ongoing truth uploads and serialize them into the @a uc + * state. + * + * @param[in,out] uc context to take truth uploads from and to update state of + */ +static void +serialize_truth (struct UploadContext *uc) +{ + json_t *policies; + + policies = json_object_get (uc->state, + "policies"); + GNUNET_assert (json_is_array (policies)); + for (struct TruthUpload *tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + if (NULL == tue->t) + continue; + for (unsigned int i = 0; ipolicies_length; i++) + { + const struct PolicyMethodReference *pmr = &tue->policies[i]; + json_t *policy = json_array_get (policies, + pmr->policy_index); + json_t *methods = json_object_get (policy, + "methods"); + json_t *auth_method = json_array_get (methods, + pmr->method_index); + json_t *truth = ANASTASIS_truth_to_json (tue->t); + + GNUNET_assert (0 == + json_object_set_new (truth, + "upload_status", + json_integer (tue->us))); + GNUNET_assert (NULL != policy); + GNUNET_assert (NULL != methods); + GNUNET_assert (NULL != auth_method); + GNUNET_assert (NULL != truth); + GNUNET_assert (0 == + json_object_set_new (auth_method, + "truth", + truth)); + } + } +} + + +/** + * Function called with the results of a #ANASTASIS_secret_share(). + * + * @param cls closure with a `struct UploadContext *` + * @param sr share result + */ +static void +secret_share_result_cb (void *cls, + const struct ANASTASIS_ShareResult *sr) +{ + struct UploadContext *uc = cls; + + uc->ss = NULL; + switch (sr->ss) + { + case ANASTASIS_SHARE_STATUS_SUCCESS: + /* Just to be safe, delete the "core_secret" so that it is not + accidentally preserved anywhere */ + (void) json_object_del (uc->state, + "core_secret"); + { + json_t *sa = json_object (); + + GNUNET_assert (NULL != sa); + for (unsigned int i = 0; idetails.success.num_providers; i++) + { + const struct ANASTASIS_ProviderSuccessStatus *pssi + = &sr->details.success.pss[i]; + json_t *d; + + d = json_pack ("{s:I, s:o}", + "policy_version", + pssi->policy_version, + "policy_expiration", + GNUNET_JSON_from_time_abs (pssi->policy_expiration)); + GNUNET_assert (NULL != d); + GNUNET_assert (0 == + json_object_set_new (sa, + pssi->provider_url, + d)); + } + GNUNET_assert (0 == + json_object_set_new (uc->state, + "success_details", + sa)); + } + set_state (uc->state, + ANASTASIS_BACKUP_STATE_BACKUP_FINISHED); + uc->cb (uc->cb_cls, + TALER_EC_NONE, + uc->state); + break; + case ANASTASIS_SHARE_STATUS_PAYMENT_REQUIRED: + { + json_t *ra; + json_t *providers; + + providers = json_object_get (uc->state, + "policy_providers"); + set_state (uc->state, + ANASTASIS_BACKUP_STATE_POLICIES_PAYING); + serialize_truth (uc); + ra = json_array (); + GNUNET_assert (NULL != ra); + for (unsigned int i = 0; i< + sr->details.payment_required.payment_requests_length; i++) + { + const struct ANASTASIS_SharePaymentRequest *spr; + json_t *pr; + size_t off; + json_t *provider; + + spr = &sr->details.payment_required.payment_requests[i]; + pr = json_pack ("{s:s, s:s}", + "payto", + spr->payment_request_url, + "provider", + spr->provider_url); + GNUNET_assert (0 == + json_array_append_new (ra, + pr)); + json_array_foreach (providers, off, provider) + { + const char *purl = json_string_value (json_object_get (provider, + "provider_url")); + + if (NULL == purl) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "policy_providers array contents are invalid"); + json_decref (ra); + return; + } + if (0 == strcmp (purl, + spr->provider_url)) + { + json_t *psj; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Remembering payment secret for provider `%s'\n", + spr->provider_url); + psj = GNUNET_JSON_from_data_auto (&spr->payment_secret); + GNUNET_assert (0 == + json_object_set_new (provider, + "payment_secret", + psj)); + } + } + } + GNUNET_assert (0 == + json_object_set_new (uc->state, + "policy_payment_requests", + ra)); + } + uc->cb (uc->cb_cls, + TALER_EC_NONE, + uc->state); + break; + case ANASTASIS_SHARE_STATUS_PROVIDER_FAILED: + { + json_t *details; + + details = json_pack ("{s:s, s:I, s:I, s:s}", + "backup_state", + "ERROR", + "http_status", + (json_int_t) sr->details.provider_failure.http_status, + "upload_status", + (json_int_t) sr->details.provider_failure.ec, + "provider_url", + sr->details.provider_failure.provider_url); + uc->cb (uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED, + details); + json_decref (details); + } + break; + default: + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected share result"); + break; + } + upload_cancel_cb (uc); +} + + +/** + * All truth uploads are done, begin with uploading the policy. + * + * @param[in,out] uc context for the operation + */ +static void +share_secret (struct UploadContext *uc) +{ + json_t *user_id; + json_t *core_secret; + json_t *jpolicies; + json_t *providers = NULL; + size_t policies_len; + const char *secret_name = NULL; + unsigned int pds_len; + struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("identity_attributes", + &user_id), + GNUNET_JSON_spec_json ("policies", + &jpolicies), + GNUNET_JSON_spec_json ("policy_providers", + &providers), + GNUNET_JSON_spec_json ("core_secret", + &core_secret), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("secret_name", + &secret_name)), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (uc->state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "State parsing failed when preparing to share secret"); + upload_cancel_cb (uc); + return; + } + + { + json_t *args; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &timeout)), + GNUNET_JSON_spec_end () + }; + + args = json_object_get (uc->state, + "pay-arguments"); + if ( (NULL != args) && + (GNUNET_OK != + GNUNET_JSON_parse (args, + pspec, + NULL, NULL)) ) + { + json_dumpf (args, + stderr, + JSON_INDENT (2)); + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + NULL); + upload_cancel_cb (uc); + return; + } + } + + if ( (! json_is_object (user_id)) || + (! json_is_array (jpolicies)) || + (0 == json_array_size (jpolicies)) || + ( (NULL != providers) && + (! json_is_array (providers)) ) ) + { + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "State parsing failed checks when preparing to share secret"); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + + policies_len = json_array_size (jpolicies); + pds_len = json_array_size (providers); + + if (0 == pds_len) + { + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "no workable providers in state"); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + + + { + struct ANASTASIS_Policy *vpolicies[policies_len]; + const struct ANASTASIS_Policy *policies[policies_len]; + struct ANASTASIS_ProviderDetails pds[GNUNET_NZL (pds_len)]; + + /* initialize policies/vpolicies arrays */ + memset (pds, + 0, + sizeof (pds)); + for (size_t i = 0; icb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'methods' must be an array"); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + methods_len = json_array_size (jmethods); + { + struct ANASTASIS_Policy *p; + struct ANASTASIS_Truth *truths[methods_len]; + const struct ANASTASIS_Truth *ctruths[methods_len]; + + for (unsigned int j = 0; jcb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'truth' failed to decode"); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + if (NULL != jtruth) + { + /* Get truth by deserializing from state */ + truths[j] = ANASTASIS_truth_from_json (jtruth); + if (NULL == truths[j]) + { + GNUNET_break (0); + for (unsigned int k = 0; kcb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'truth' failed to decode"); + GNUNET_JSON_parse_free (ispec); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + } + else + { + bool found = false; + /* Maybe we never serialized the truth; find it in our DLL */ + for (struct TruthUpload *tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + GNUNET_break (NULL != tue->t); + if ( (tue->am_idx == truth_index) && + (0 == strcmp (provider_url, + tue->provider_url)) ) + { + /* Duplicate truth object */ + json_t *jt = ANASTASIS_truth_to_json (tue->t); + + GNUNET_assert (NULL != jt); + truths[j] = ANASTASIS_truth_from_json (jt); + GNUNET_assert (NULL != truths[j]); + json_decref (jt); + found = true; + break; + } + } + if (! found) + { + GNUNET_break (0); + for (unsigned int k = 0; kcb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'truth' failed to decode"); + GNUNET_JSON_parse_free (ispec); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return; + } + } + GNUNET_JSON_parse_free (ispec); + ctruths[j] = truths[j]; + } + p = ANASTASIS_policy_create (ctruths, + methods_len); + vpolicies[i] = p; + policies[i] = p; + for (unsigned int k = 0; kstate, + pds[i].provider_url, + &pds[i].provider_salt)) ) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'providers' entry malformed"); + for (unsigned int i = 0; iss = ANASTASIS_secret_share (ANASTASIS_REDUX_ctx_, + user_id, + pds, + pds_len, + policies, + policies_len, + uc->years, + timeout, + &secret_share_result_cb, + uc, + secret_name, + secret, + secret_size); + GNUNET_free (secret); + } + for (unsigned int i = 0; iss) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (uc->cb, + uc->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Failed to begin secret sharing"); + upload_cancel_cb (uc); + return; + } +} + + +/** + * Some truth uploads require payment, serialize state and + * request payment to be executed by the application. + * + * @param[in,out] uc context for the operation + */ +static void +request_truth_payment (struct UploadContext *uc) +{ + json_t *payments; + + payments = json_array (); + GNUNET_assert (NULL != payments); + serialize_truth (uc); + for (struct TruthUpload *tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + if (NULL == tue->payment_request) + continue; + GNUNET_assert ( + 0 == + json_array_append_new (payments, + json_string ( + tue->payment_request))); + } + GNUNET_assert (0 == + json_object_set_new (uc->state, + "payments", + payments)); + set_state (uc->state, + ANASTASIS_BACKUP_STATE_TRUTHS_PAYING); + uc->cb (uc->cb_cls, + TALER_EC_NONE, + uc->state); + upload_cancel_cb (uc); +} + + +/** + * We may be finished with all (active) asynchronous operations. + * Check if any are pending and continue accordingly. + * + * @param[in,out] uc context for the operation + */ +static void +check_upload_finished (struct UploadContext *uc) +{ + bool pay = false; + bool active = false; + + for (struct TruthUpload *tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + if (TALER_EC_NONE != tue->ec) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Truth upload failed with error %d\n", + (int) tue->ec); + uc->cb (uc->cb_cls, + tue->ec, + NULL); + upload_cancel_cb (uc); + return; + } + if (NULL != tue->tu) + active = true; + if (NULL != tue->payment_request) + pay = true; + } + if (active) + return; + if (pay) + { + request_truth_payment (uc); + return; + } + share_secret (uc); +} + + +/** + * 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 with a `struct TruthUpload` + * @param t truth object to create policies, NULL on failure + * @param ud upload details + */ +static void +truth_upload_cb (void *cls, + struct ANASTASIS_Truth *t, + const struct ANASTASIS_UploadDetails *ud) +{ + struct TruthUpload *tue = cls; + + tue->tu = NULL; + tue->t = t; + tue->ec = ud->ec; + tue->us = ud->us; + if (ANASTASIS_US_PAYMENT_REQUIRED == ud->us) + { + tue->payment_request = GNUNET_strdup ( + ud->details.payment.payment_request); + } + check_upload_finished (tue->uc); +} + + +/** + * Check if we still need to create a new truth object for the truth + * identified by @a provider_url and @a am_idx. If so, create it from + * @a truth for policy reference @a pmr. If such a truth object + * already exists, append @a pmr to its list of reasons. + * + * @param[in,out] our upload context + * @param pmr policy method combination that requires the truth + * @param provider_url the URL of the Anastasis provider to upload + * the truth to, used to check for existing entries + * @param am_idx index of the authentication method, used to check for existing entries + * @param[in] truth object representing already uploaded truth, reference captured! + * @param[in,out] async_truth pointer to counter with the number of ongoing uploads, + * updated + * @param auth_method object with the challenge details, to generate the truth + * @return #GNUNET_SYSERR error requiring abort, + * #GNUNET_OK on success + */ +static int +add_truth_object (struct UploadContext *uc, + const struct PolicyMethodReference *pmr, + const char *provider_url, + uint32_t am_idx, + json_t *truth, + unsigned int *async_truth, + json_t *auth_method) +{ + /* check if we are already uploading this truth */ + struct TruthUpload *tue; + bool must_upload = true; + + for (tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + if ( (0 == strcmp (tue->provider_url, + provider_url)) && + (am_idx == tue->am_idx) ) + { + GNUNET_array_append (tue->policies, + tue->policies_length, + *pmr); + break; + } + } + + if (NULL == tue) + { + /* Create new entry */ + tue = GNUNET_new (struct TruthUpload); + + GNUNET_CONTAINER_DLL_insert (uc->tues_head, + uc->tues_tail, + tue); + tue->uc = uc; + tue->policies = GNUNET_new (struct PolicyMethodReference); + *tue->policies = *pmr; + tue->provider_url = GNUNET_strdup (provider_url); + tue->am_idx = am_idx; + tue->policies_length = 1; + } + + { + uint32_t status = UINT32_MAX; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("upload_status", + &status)), + GNUNET_JSON_spec_end () + }; + if (GNUNET_OK != + GNUNET_JSON_parse (truth, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + must_upload = (ANASTASIS_US_SUCCESS != status); + } + + if (NULL == tue->t) + { + tue->t = ANASTASIS_truth_from_json (truth); + if (NULL == tue->t) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + } + + if ( (NULL != tue->tu) && + (! must_upload) ) + { + ANASTASIS_truth_upload_cancel (tue->tu); + (*async_truth)--; + tue->tu = NULL; + return GNUNET_OK; + } + + if ( (NULL == tue->tu) && + (must_upload) ) + { + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + struct ANASTASIS_CRYPTO_UserIdentifierP id; + void *truth_data; + size_t truth_data_size; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_varsize ("challenge", + &truth_data, + &truth_data_size), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + lookup_salt (uc->state, + provider_url, + &salt)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_JSON_parse (auth_method, + spec, + NULL, NULL)) + { + json_dumpf (auth_method, + stderr, + JSON_INDENT (2)); + GNUNET_break (0); + return GNUNET_SYSERR; + } + { + json_t *user_id; + + user_id = json_object_get (uc->state, + "identity_attributes"); + if (! json_is_object (user_id)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + ANASTASIS_CRYPTO_user_identifier_derive (user_id, + &salt, + &id); + } + tue->tu = ANASTASIS_truth_upload3 (ANASTASIS_REDUX_ctx_, + &id, + tue->t, + truth_data, + truth_data_size, + uc->years, + uc->timeout, + &truth_upload_cb, + tue); + GNUNET_JSON_parse_free (spec); + tue->t = NULL; + (*async_truth)++; + } + + if ( (NULL != tue->tu) && + (NULL != tue->t) ) + { + /* no point in having both */ + ANASTASIS_truth_free (tue->t); + tue->t = NULL; + } + return GNUNET_OK; +} + + +/** + * Check if we still need to upload the truth identified by + * @a provider_url and @a am_idx. If so, upload it for + * policy reference @a pmr. If the upload is already queued, + * append @a pmr to its list of reasons. + * + * @param[in,out] our upload context + * @param pmr policy method combination that requires the truth + * @param provider_url the URL of the Anastasis provider to upload + * the truth to, used to check for existing entries + * @param am_idx index of the authentication method, used to check for existing entries + * @param auth_method object with the challenge details, to generate the truth + * @return #GNUNET_SYSERR on error requiring abort, + * #GNUNET_NO if no new truth upload was generated (@a pmr was appended) + * #GNUNET_OK if a new truth upload was initiated + */ +static int +check_truth_upload (struct UploadContext *uc, + const struct PolicyMethodReference *pmr, + const char *provider_url, + uint32_t am_idx, + json_t *auth_method) +{ + json_t *user_id; + json_t *jtruth; + struct TruthUpload *tue; + + user_id = json_object_get (uc->state, + "identity_attributes"); + if (! json_is_object (user_id)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* check if we are already uploading this truth */ + for (tue = uc->tues_head; + NULL != tue; + tue = tue->next) + { + if ( (0 == strcmp (tue->provider_url, + provider_url)) && + (am_idx == tue->am_idx) ) + { + GNUNET_array_append (tue->policies, + tue->policies_length, + *pmr); + return GNUNET_NO; + } + } + + /* need new upload */ + tue = GNUNET_new (struct TruthUpload); + { + json_t *policies = json_object_get (uc->state, + "policies"); + json_t *policy = json_array_get (policies, + pmr->policy_index); + json_t *methods = json_object_get (policy, + "methods"); + json_t *method = json_array_get (methods, + pmr->method_index); + + jtruth = json_object_get (method, + "truth"); + } + + { + const char *type; + const char *mime_type = NULL; + const char *instructions = NULL; + void *truth_data; + size_t truth_data_size; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("mime_type", + &mime_type)), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("instructions", + &instructions)), + GNUNET_JSON_spec_varsize ("challenge", + &truth_data, + &truth_data_size), + GNUNET_JSON_spec_end () + }; + struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt; + struct ANASTASIS_CRYPTO_UserIdentifierP id; + + if (GNUNET_OK != + GNUNET_JSON_parse (auth_method, + spec, + NULL, NULL)) + { + json_dumpf (auth_method, + stderr, + JSON_INDENT (2)); + GNUNET_break (0); + GNUNET_free (tue); + return GNUNET_SYSERR; + } + GNUNET_CONTAINER_DLL_insert (uc->tues_head, + uc->tues_tail, + tue); + tue->uc = uc; + tue->policies = GNUNET_new (struct PolicyMethodReference); + *tue->policies = *pmr; + tue->provider_url = GNUNET_strdup (provider_url); + tue->am_idx = am_idx; + tue->policies_length = 1; + if (GNUNET_OK != + lookup_salt (uc->state, + provider_url, + &provider_salt)) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return GNUNET_SYSERR; + } + ANASTASIS_CRYPTO_user_identifier_derive (user_id, + &provider_salt, + &id); + { + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + struct ANASTASIS_CRYPTO_QuestionSaltP question_salt; + struct ANASTASIS_CRYPTO_TruthKeyP truth_key; + struct ANASTASIS_CRYPTO_KeyShareP key_share; + struct ANASTASIS_CRYPTO_NonceP nonce; + + struct GNUNET_JSON_Specification jspec[] = { + GNUNET_JSON_spec_fixed_auto ("salt", + &question_salt), + GNUNET_JSON_spec_fixed_auto ("truth_key", + &truth_key), + GNUNET_JSON_spec_fixed_auto ("nonce", + &nonce), + GNUNET_JSON_spec_fixed_auto ("uuid", + &uuid), + GNUNET_JSON_spec_fixed_auto ("key_share", + &key_share), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (jtruth, + jspec, + NULL, NULL)) + { + tue->tu = ANASTASIS_truth_upload (ANASTASIS_REDUX_ctx_, + &id, + provider_url, + type, + instructions, + mime_type, + &provider_salt, + truth_data, + truth_data_size, + uc->years, + uc->timeout, + &truth_upload_cb, + tue); + } + else + { + tue->tu = ANASTASIS_truth_upload2 (ANASTASIS_REDUX_ctx_, + &id, + provider_url, + type, + instructions, + mime_type, + &provider_salt, + truth_data, + truth_data_size, + uc->years, + uc->timeout, + &nonce, + &uuid, + &question_salt, + &truth_key, + &key_share, + &truth_upload_cb, + tue); + } + } + if (NULL == tue->tu) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + upload_cancel_cb (uc); + return GNUNET_SYSERR; + } + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; + } +} + + +/** + * Function to upload truths and recovery document policies. + * Ultimately transitions to failed state (allowing user to go back + * and change providers/policies), or payment, or finished. + * + * @param state state to operate on + * @param truth_indices indices of truths to upload explicitly + * @param cb callback (#ANASTASIS_ActionCallback) to call after upload + * @param cb_cls callback closure + */ +static struct ANASTASIS_ReduxAction * +upload (json_t *state, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct UploadContext *uc; + json_t *auth_methods; + json_t *policies; + struct GNUNET_TIME_Absolute expiration; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_absolute_time ("expiration", + &expiration), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'expiration' missing"); + return NULL; + } + auth_methods = json_object_get (state, + "authentication_methods"); + if ( (! json_is_array (auth_methods)) || + (0 == json_array_size (auth_methods)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_methods' must be non-empty array"); + return NULL; + } + policies = json_object_get (state, + "policies"); + if ( (! json_is_array (policies)) || + (0 == json_array_size (policies)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' must be non-empty array"); + return NULL; + } + + uc = GNUNET_new (struct UploadContext); + uc->ra.cleanup = &upload_cancel_cb; + uc->ra.cleanup_cls = uc; + uc->cb = cb; + uc->cb_cls = cb_cls; + uc->state = json_incref (state); + uc->years = expiration_to_years (expiration); + + { + json_t *args; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &uc->timeout)), + GNUNET_JSON_spec_end () + }; + + args = json_object_get (uc->state, + "pay-arguments"); + if ( (NULL != args) && + (GNUNET_OK != + GNUNET_JSON_parse (args, + pspec, + NULL, NULL)) ) + { + json_dumpf (args, + stderr, + JSON_INDENT (2)); + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'timeout' must be valid delay"); + + return NULL; + } + } + + { + json_t *policy; + size_t pindex; + unsigned int async_truth = 0; + + json_array_foreach (policies, pindex, policy) + { + json_t *methods = json_object_get (policy, + "methods"); + json_t *auth_method; + size_t mindex; + + if ( (! json_is_array (methods)) || + (0 == json_array_size (policies)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'policies' must be non-empty array"); + upload_cancel_cb (uc); + return NULL; + } + json_array_foreach (methods, mindex, auth_method) + { + uint32_t am_idx; + const char *provider_url; + json_t *truth = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("provider", + &provider_url), + GNUNET_JSON_spec_uint32 ("authentication_method", + &am_idx), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("truth", + &truth)), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (auth_method, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'method' data malformed"); + upload_cancel_cb (uc); + return NULL; + } + { + struct PolicyMethodReference pmr = { + .policy_index = pindex, + .method_index = mindex + }; + json_t *amj; + + amj = json_array_get (auth_methods, + am_idx); + if (NULL == amj) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_method' refers to invalid authorization index malformed"); + upload_cancel_cb (uc); + GNUNET_JSON_parse_free (spec); + return NULL; + } + if (NULL == truth) + { + int ret; + + ret = check_truth_upload (uc, + &pmr, + provider_url, + am_idx, + amj); + if (GNUNET_SYSERR == ret) + { + GNUNET_JSON_parse_free (spec); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + NULL); + return NULL; + } + if (GNUNET_OK == ret) + async_truth++; + } + else + { + int ret; + + ret = add_truth_object (uc, + &pmr, + provider_url, + am_idx, + truth, + &async_truth, + amj); + if (GNUNET_SYSERR == ret) + { + GNUNET_JSON_parse_free (spec); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + NULL); + return NULL; + } + } + } + GNUNET_JSON_parse_free (spec); + } /* end for all methods of policy */ + } /* end for all policies */ + if (async_truth > 0) + return &uc->ra; + } + share_secret (uc); + if (NULL == uc->ss) + return NULL; + return &uc->ra; +} + + +/** + * Test if the core secret @a secret_size is small enough to be stored + * at all providers, which have a minimum upload limit of @a min_limit_in_mb. + * + * For now, we do not precisely calculate the size of the recovery document, + * and simply assume that the instructions (i.e. security questions) are all + * relatively small (aka sane), and that the number of authentication methods + * and recovery policies is similarly small so that all of this meta data + * fits in 512 kb (which is VERY big). + * + * Even with the minimum permitted upload limit of 1 MB (which is likely, + * given that there is hardly a reason for providers to offer more), this + * leaves 512 kb for the @a secret_size, which should be plenty (given + * that this is supposed to be for a master key, and not the actual data). + * + * @param state our state, could be used in the future to calculate the + * size of the recovery document without the core secret + * @param secret_size size of the core secret + * @param min_limit_in_mb minimum upload size of all providers + */ +static bool +core_secret_fits (const json_t *state, + size_t secret_size, + uint32_t min_limit_in_mb) +{ + return (min_limit_in_mb * 1024LL * 1024LL > + 512LLU * 1024LLU + secret_size); +} + + +/** + * Check if the upload size limit is satisfied. + * + * @param state our state + * @param jsecret the uploaded secret + * @return #GNUNET_OK if @a secret_size works for all providers, + * #GNUNET_NO if the @a secret_size is too big, + * #GNUNET_SYSERR if a provider has a limit of 0 + */ +static enum GNUNET_GenericReturnValue +check_upload_size_limit (json_t *state, + const json_t *jsecret) +{ + uint32_t min_limit = UINT32_MAX; + json_t *aps = json_object_get (state, + "authentication_providers"); + const char *url; + json_t *ap; + size_t secret_size; + + { + char *secret; + + secret = json_dumps (jsecret, + JSON_COMPACT | JSON_SORT_KEYS); + GNUNET_assert (NULL != secret); + secret_size = strlen (secret); + GNUNET_free (secret); + } + + /* We calculate the minimum upload limit of all possible providers; + this is under the (simplified) assumption that we store the + recovery document at all providers; this may be changed later, + see #6760. */ + json_object_foreach (aps, url, ap) + { + uint32_t limit; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", + &limit), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (ap, + spec, + NULL, NULL)) + { + /* skip malformed provider, likely /config failed */ + continue; + } + if (0 == limit) + return GNUNET_SYSERR; + min_limit = GNUNET_MIN (min_limit, + limit); + } + if (! core_secret_fits (state, + secret_size, + min_limit)) + return GNUNET_NO; + return GNUNET_OK; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "enter_secret" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +enter_secret (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *jsecret; + struct GNUNET_TIME_Absolute expiration = {0}; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("secret", + &jsecret), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_absolute_time ("expiration", + &expiration)), + GNUNET_JSON_spec_end () + }; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (arguments, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'secret' argument required"); + return NULL; + } + + /* check upload size limit */ + { + enum GNUNET_GenericReturnValue ret; + + ret = check_upload_size_limit (state, + jsecret); + switch (ret) + { + case GNUNET_SYSERR: + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "provider has an upload limit of 0"); + return NULL; + case GNUNET_NO: + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_SECRET_TOO_BIG, + NULL); + return NULL; + default: + break; + } + } + if (0 != expiration.abs_value_us) + { + if (GNUNET_OK != + update_expiration_cost (state, + expiration)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "could not calculate expiration cost"); + return NULL; + } + } + GNUNET_assert (0 == + json_object_set (state, + "core_secret", + jsecret)); + cb (cb_cls, + TALER_EC_NONE, + state); + GNUNET_JSON_parse_free (spec); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "clear_secret" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +clear_secret (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + if (0 != + json_object_del (state, + "core_secret")) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'core_secret' not set"); + return NULL; + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for an + * "enter_secret_name" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +enter_secret_name (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const char *secret_name = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &secret_name), + GNUNET_JSON_spec_end () + }; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (arguments, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'name' argument required"); + return NULL; + } + + GNUNET_assert (0 == + json_object_set_new (state, + "secret_name", + json_string (secret_name))); + cb (cb_cls, + TALER_EC_NONE, + state); + GNUNET_JSON_parse_free (spec); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for the + * "update_expiration" action in the "secret editing" state. + * Updates how long we are to store the truth and policies + * and computes the new cost. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL (synchronous operation) + */ +static struct ANASTASIS_ReduxAction * +update_expiration (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct GNUNET_TIME_Absolute expiration; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_absolute_time ("expiration", + &expiration), + GNUNET_JSON_spec_end () + }; + + if (NULL == arguments) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (arguments, + spec, + NULL, NULL)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'expiration' argument required"); + return NULL; + } + if (GNUNET_OK != + update_expiration_cost (state, + expiration)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "could not calculate expiration cost"); + return NULL; + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for the + * "next" action in the "secret editing" state. + * Returns an #ANASTASIS_ReduxAction as operation is async. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + */ +static struct ANASTASIS_ReduxAction * +finish_secret (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *core_secret; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("core_secret", + &core_secret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "State parsing failed: 'core_secret' is missing"); + return NULL; + } + + /* check upload size limit */ + { + enum GNUNET_GenericReturnValue ret; + + ret = check_upload_size_limit (state, + core_secret); + switch (ret) + { + case GNUNET_SYSERR: + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "provider has an upload limit of 0"); + GNUNET_JSON_parse_free (spec); + return NULL; + case GNUNET_NO: + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_SECRET_TOO_BIG, + NULL); + GNUNET_JSON_parse_free (spec); + return NULL; + default: + break; + } + } + + GNUNET_JSON_parse_free (spec); + return upload (state, + cb, + cb_cls); +} + + +/** + * DispatchHandler/Callback function which is called for a + * "pay" action. + * Returns an #ANASTASIS_ReduxAction as operation is async. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + */ +static struct ANASTASIS_ReduxAction * +pay_truths_backup (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + /* Clear 'payments' if it exists */ + (void) json_object_del (state, + "payments"); + if (NULL != arguments) + GNUNET_assert (0 == + json_object_set (state, + "pay-arguments", + (json_t *) arguments)); + return upload (state, + cb, + cb_cls); +} + + +/** + * DispatchHandler/Callback function which is called for a + * "pay" action. + * Returns an #ANASTASIS_ReduxAction as operation is async. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + */ +static struct ANASTASIS_ReduxAction * +pay_policies_backup (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + /* Clear 'policy_payment_requests' if it exists */ + (void) json_object_del (state, + "policy_payment_requests"); + if (NULL != arguments) + GNUNET_assert (0 == + json_object_set (state, + "pay-arguments", + (json_t *) arguments)); + return upload (state, + cb, + cb_cls); +} + + +/** + * DispatchHandler/Callback function which is called for a + * "back" action if state is "FINISHED". + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +back_finished (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + set_state (state, + ANASTASIS_BACKUP_STATE_SECRET_EDITING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * Signature of callback function that implements a state transition. + * + * @param state current state + * @param arguments arguments for the state transition + * @param cb function to call when done + * @param cb_cls closure for @a cb + */ +typedef struct ANASTASIS_ReduxAction * +(*DispatchHandler)(json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * 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[in,out] state input/output state (to be modified) + * @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 + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_backup_action_ (json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct Dispatcher + { + enum ANASTASIS_BackupState backup_state; + const char *backup_action; + DispatchHandler fun; + } dispatchers[] = { + { + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING, + "add_authentication", + &add_authentication + }, + { + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING, + "delete_authentication", + &del_authentication + }, + { + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING, + "next", + &done_authentication + }, + { + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING, + "add_provider", + &add_provider + }, + { + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "add_policy", + &add_policy + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "update_policy", + &update_policy + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "delete_policy", + &del_policy + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "delete_challenge", + &del_challenge + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "next", + &done_policy_review + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_REVIEWING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "enter_secret", + &enter_secret + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "clear_secret", + &clear_secret + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "enter_secret_name", + &enter_secret_name + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "update_expiration", + &update_expiration + }, + { + ANASTASIS_BACKUP_STATE_SECRET_EDITING, + "next", + &finish_secret + }, + { + ANASTASIS_BACKUP_STATE_TRUTHS_PAYING, + "pay", + &pay_truths_backup + }, + { + ANASTASIS_BACKUP_STATE_POLICIES_PAYING, + "pay", + &pay_policies_backup + }, + { + ANASTASIS_BACKUP_STATE_BACKUP_FINISHED, + "back", + &back_finished + }, + { ANASTASIS_BACKUP_STATE_ERROR, NULL, NULL } + }; + const char *s = json_string_value (json_object_get (state, + "backup_state")); + enum ANASTASIS_BackupState bs; + + GNUNET_assert (NULL != s); /* holds as per invariant of caller */ + bs = ANASTASIS_backup_state_from_string_ (s); + if (ANASTASIS_BACKUP_STATE_ERROR == bs) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "unknown 'backup_state'"); + return NULL; + } + for (unsigned int i = 0; NULL != dispatchers[i].fun; i++) + { + if ( (bs == dispatchers[i].backup_state) && + (0 == strcmp (action, + dispatchers[i].backup_action)) ) + { + return dispatchers[i].fun (state, + arguments, + cb, + cb_cls); + } + } + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID, + action); + return NULL; +} + + +/** + * State for a #ANASTASIS_REDUX_backup_begin_() operation. + */ +struct BackupStartState; + + +/** + * Entry in the list of all known applicable Anastasis providers. + * Used to wait for it to complete downloading /config. + */ +struct BackupStartStateProviderEntry +{ + /** + * Kept in a DLL. + */ + struct BackupStartStateProviderEntry *next; + + /** + * Kept in a DLL. + */ + struct BackupStartStateProviderEntry *prev; + + /** + * Main operation this entry is part of. + */ + struct BackupStartState *bss; + + /** + * Resulting provider information, NULL if not (yet) available. + */ + json_t *istate; + + /** + * Ongoing reducer action to obtain /config, NULL if completed. + */ + struct ANASTASIS_ReduxAction *ra; + + /** + * Final result of the operation (once completed). + */ + enum TALER_ErrorCode ec; +}; + + +struct BackupStartState +{ + /** + * Head of list of provider /config operations we are doing. + */ + struct BackupStartStateProviderEntry *pe_head; + + /** + * Tail of list of provider /config operations we are doing. + */ + struct BackupStartStateProviderEntry *pe_tail; + + /** + * State we are updating. + */ + json_t *state; + + /** + * Function to call when we are done. + */ + ANASTASIS_ActionCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Redux action we returned to our controller. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * Number of provider /config operations in @e ba_head that + * are still awaiting completion. + */ + unsigned int pending; +}; + + +/** + * The backup start operation is being aborted, terminate. + * + * @param cls a `struct BackupStartState *` + */ +static void +abort_backup_begin_cb (void *cls) +{ + struct BackupStartState *bss = cls; + struct BackupStartStateProviderEntry *pe; + + while (NULL != (pe = bss->pe_head)) + { + GNUNET_CONTAINER_DLL_remove (bss->pe_head, + bss->pe_tail, + pe); + if (NULL != pe->ra) + pe->ra->cleanup (pe->ra->cleanup_cls); + json_decref (pe->istate); + GNUNET_free (pe); + } + json_decref (bss->state); + GNUNET_free (bss); +} + + +/** + * We finished downloading /config from all providers, merge + * into the main state, trigger the continuation and free our + * state. + * + * @param[in] bss main state to merge into + */ +static void +providers_complete (struct BackupStartState *bss) +{ + struct BackupStartStateProviderEntry *pe; + json_t *tlist; + + tlist = json_object_get (bss->state, + "authentication_providers"); + if (NULL == tlist) + { + tlist = json_object (); + GNUNET_assert (NULL != tlist); + GNUNET_assert (0 == + json_object_set_new (bss->state, + "authentication_providers", + tlist)); + } + while (NULL != (pe = bss->pe_head)) + { + json_t *provider_list; + + GNUNET_CONTAINER_DLL_remove (bss->pe_head, + bss->pe_tail, + pe); + provider_list = json_object_get (pe->istate, + "authentication_providers"); + /* merge provider_list into tlist (overriding existing entries) */ + if (NULL != provider_list) + { + const char *url; + json_t *value; + + json_object_foreach (provider_list, url, value) { + GNUNET_assert (0 == + json_object_set (tlist, + url, + value)); + } + } + json_decref (pe->istate); + GNUNET_free (pe); + } + bss->cb (bss->cb_cls, + TALER_EC_NONE, + bss->state); + json_decref (bss->state); + GNUNET_free (bss); +} + + +/** + * Function called when the complete information about a provider + * was added to @a new_state. + * + * @param cls a `struct BackupStartStateProviderEntry` + * @param error error code + * @param new_state resulting new state + */ +static void +provider_added_cb (void *cls, + enum TALER_ErrorCode error, + json_t *new_state) +{ + struct BackupStartStateProviderEntry *pe = cls; + + pe->ra = NULL; + pe->istate = json_incref (new_state); + pe->ec = error; + pe->bss->pending--; + if (0 == pe->bss->pending) + providers_complete (pe->bss); +} + + +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_backup_begin_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *provider_list; + struct BackupStartState *bss; + + provider_list = json_object_get (state, + "authentication_providers"); + if (NULL == provider_list) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_providers' missing"); + return NULL; + } + bss = GNUNET_new (struct BackupStartState); + bss->state = json_incref (state); + bss->cb = cb; + bss->cb_cls = cb_cls; + bss->ra.cleanup_cls = bss; + bss->ra.cleanup = &abort_backup_begin_cb; + bss->pending = 1; /* decremented after initialization loop */ + + { + json_t *prov; + const char *url; + json_object_foreach (provider_list, url, prov) { + struct BackupStartStateProviderEntry *pe; + json_t *istate; + + pe = GNUNET_new (struct BackupStartStateProviderEntry); + pe->bss = bss; + istate = json_object (); + GNUNET_assert (NULL != istate); + GNUNET_CONTAINER_DLL_insert (bss->pe_head, + bss->pe_tail, + pe); + pe->ra = ANASTASIS_REDUX_add_provider_to_state_ (url, + istate, + &provider_added_cb, + pe); + json_decref (istate); + if (NULL != pe->ra) + bss->pending++; + } + } + bss->pending--; + if (0 == bss->pending) + { + providers_complete (bss); + return NULL; + } + return &bss->ra; +} diff --git a/src/reducer/anastasis_api_recovery_redux.c b/src/reducer/anastasis_api_recovery_redux.c new file mode 100644 index 0000000..8a900ec --- /dev/null +++ b/src/reducer/anastasis_api_recovery_redux.c @@ -0,0 +1,2558 @@ +/* + 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 +*/ +/** + * @file lib/anastasis_api_recovery_redux.c + * @brief anastasis reducer recovery api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ + +#include +#include +#include "anastasis_redux.h" +#include "anastasis_error_codes.h" +#include "anastasis_api_redux.h" + + +#define GENERATE_STRING(STRING) #STRING, +static const char *recovery_strings[] = { + ANASTASIS_RECOVERY_STATES (GENERATE_STRING) +}; +#undef GENERATE_STRING + + +enum ANASTASIS_RecoveryState +ANASTASIS_recovery_state_from_string_ (const char *state_string) +{ + for (enum ANASTASIS_RecoveryState i = 0; + i < sizeof (recovery_strings) / sizeof(*recovery_strings); + i++) + if (0 == strcmp (state_string, + recovery_strings[i])) + return i; + return ANASTASIS_RECOVERY_STATE_ERROR; +} + + +const char * +ANASTASIS_recovery_state_to_string_ (enum ANASTASIS_RecoveryState rs) +{ + if ( (rs < 0) || + (rs >= sizeof (recovery_strings) / sizeof(*recovery_strings)) ) + { + GNUNET_break_op (0); + return NULL; + } + return recovery_strings[rs]; +} + + +static void +set_state (json_t *state, + enum ANASTASIS_RecoveryState new_recovery_state) +{ + GNUNET_assert ( + 0 == + json_object_set_new ( + state, + "recovery_state", + json_string (ANASTASIS_recovery_state_to_string_ (new_recovery_state)))); +} + + +/** + * Returns an initial ANASTASIS recovery state. + * + * @return NULL on failure + */ +json_t * +ANASTASIS_recovery_start (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + json_t *initial_state; + + (void) cfg; + initial_state = ANASTASIS_REDUX_load_continents_ (); + if (NULL == initial_state) + return NULL; + set_state (initial_state, + ANASTASIS_RECOVERY_STATE_CONTINENT_SELECTING); + return initial_state; +} + + +/** + * Context for a "select_challenge" operation. + */ +struct SelectChallengeContext +{ + /** + * Handle we returned for cancellation of the operation. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * UUID of the challenge selected by the user for solving. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + + /** + * Which timeout was set for the operation? + */ + struct GNUNET_TIME_Relative timeout; + + /** + * Overall recovery action. + */ + struct ANASTASIS_Recovery *r; + + /** + * Function to call with the next state. + */ + ANASTASIS_ActionCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Our state. + */ + json_t *state; + + /** + * Our arguments (like answers to the challenge, if already provided). + */ + json_t *args; + + /** + * Task scheduled for delayed success reporting. Needed to make + * sure that the solved challenge was really the final result, + * cancelled if the solved challenge resulted in the secret being + * recovered. + */ + struct GNUNET_SCHEDULER_Task *delayed_report; + + /** + * Payment secret, if we are in the "pay" state. + */ + struct ANASTASIS_PaymentSecretP ps; +}; + + +/** + * Cleanup a select challenge context. + * + * @param cls a `struct SelectChallengeContext *` + */ +static void +sctx_free (void *cls) +{ + struct SelectChallengeContext *sctx = cls; + + if (NULL != sctx->r) + { + ANASTASIS_recovery_abort (sctx->r); + sctx->r = NULL; + } + json_decref (sctx->state); + json_decref (sctx->args); + if (NULL != sctx->delayed_report) + { + GNUNET_SCHEDULER_cancel (sctx->delayed_report); + sctx->delayed_report = NULL; + } + GNUNET_free (sctx); +} + + +/** + * Update @a state to reflect the error provided in @a rc. + * + * @param[in,out] state state to update + * @param rc error code to translate to JSON + * @return error code to use + */ +static enum TALER_ErrorCode +update_state_by_error (json_t *state, + enum ANASTASIS_RecoveryStatus rc) +{ + const char *msg = NULL; + enum TALER_ErrorCode ec = TALER_EC_INVALID; + + switch (rc) + { + case ANASTASIS_RS_SUCCESS: + GNUNET_assert (0); + break; + case ANASTASIS_RS_POLICY_DOWNLOAD_FAILED: + msg = gettext_noop ("download failed due to unexpected network issue"); + ec = TALER_EC_ANASTASIS_REDUCER_NETWORK_FAILED; + break; + case ANASTASIS_RS_POLICY_DOWNLOAD_NO_POLICY: + GNUNET_break (0); + msg = gettext_noop ("policy document returned was malformed"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_POLICY_DOWNLOAD_TOO_BIG: + GNUNET_break (0); + msg = gettext_noop ("policy document too large for client memory"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_POLICY_DOWNLOAD_INVALID_COMPRESSION: + GNUNET_break (0); + msg = gettext_noop ("failed to decompress policy document"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_POLICY_DOWNLOAD_NO_JSON: + GNUNET_break (0); + msg = gettext_noop ("policy document returned was not in JSON format"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_POLICY_MALFORMED_JSON: + GNUNET_break (0); + msg = gettext_noop ( + "policy document returned was not in required JSON format"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED; + break; + case ANASTASIS_RS_POLICY_SERVER_ERROR: + msg = gettext_noop ("Anastasis server reported transient internal error"); + ec = TALER_EC_ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED; + break; + case ANASTASIS_RS_POLICY_GONE: + msg = gettext_noop ("policy document no longer exists"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED; + break; + case ANASTASIS_RS_POLICY_UNKNOWN: + msg = gettext_noop ("account unknown to Anastasis server"); + ec = TALER_EC_ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED; + break; + } + GNUNET_assert (0 == + json_object_set_new (state, + "error_message", + json_string (msg))); + GNUNET_assert (0 == + json_object_set_new (state, + "error_code", + json_integer (rc))); + set_state (state, + ANASTASIS_GENERIC_STATE_ERROR); + return ec; +} + + +/** + * This function is called whenever the recovery process ends. + * On success, the secret is returned in @a secret. + * + * @param cls handle for the callback + * @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 + */ +static void +core_secret_cb (void *cls, + enum ANASTASIS_RecoveryStatus rc, + const void *secret, + size_t secret_size) +{ + struct SelectChallengeContext *sctx = cls; + enum TALER_ErrorCode ec; + + sctx->r = NULL; + if (ANASTASIS_RS_SUCCESS == rc) + { + json_t *jsecret; + + jsecret = json_loadb (secret, + secret_size, + JSON_REJECT_DUPLICATES, + NULL); + if (NULL == jsecret) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_SECRET_MALFORMED, + NULL); + sctx_free (sctx); + return; + } + GNUNET_assert (0 == + json_object_set_new (sctx->state, + "core_secret", + jsecret)); + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_RECOVERY_FINISHED); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + } + ec = update_state_by_error (sctx->state, + rc); + sctx->cb (sctx->cb_cls, + ec, + sctx->state); + sctx_free (sctx); +} + + +/** + * A challenge was solved, but we are not yet finished. + * Report to caller that the challenge was completed. + * + * @param cls a `struct SelectChallengeContext` + */ +static void +report_solved (void *cls) +{ + struct SelectChallengeContext *sctx = cls; + + sctx->delayed_report = NULL; + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); +} + + +/** + * Find challenge of @a uuid in @a state under "recovery_information". + * + * @param state the state to search + * @param uuid the UUID to search for + * @return NULL on error, otherwise challenge entry; RC is NOT incremented + */ +static json_t * +find_challenge_in_ri (json_t *state, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid) +{ + struct ANASTASIS_CRYPTO_TruthUUIDP u; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("uuid", + &u), + GNUNET_JSON_spec_end () + }; + json_t *ri; + json_t *challenges; + json_t *challenge; + size_t index; + + ri = json_object_get (state, + "recovery_information"); + if (NULL == ri) + { + GNUNET_break (0); + return NULL; + } + challenges = json_object_get (ri, + "challenges"); + if (NULL == challenges) + { + GNUNET_break (0); + return NULL; + } + json_array_foreach (challenges, index, challenge) + { + if (GNUNET_OK != + GNUNET_JSON_parse (challenge, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + if (0 == + GNUNET_memcmp (&u, + uuid)) + { + return challenge; + } + } + return NULL; +} + + +/** + * Defines a callback for the response status for a challenge start + * operation. + * + * @param cls a `struct SelectChallengeContext *` + * @param csr response details + */ +static void +answer_feedback_cb ( + void *cls, + const struct ANASTASIS_ChallengeStartResponse *csr) +{ + struct SelectChallengeContext *sctx = cls; + const struct ANASTASIS_ChallengeDetails *cd; + char uuid[sizeof (cd->uuid) * 2]; + char *end; + json_t *feedback; + + cd = ANASTASIS_challenge_get_details (csr->challenge); + end = GNUNET_STRINGS_data_to_string (&cd->uuid, + sizeof (cd->uuid), + uuid, + sizeof (uuid)); + GNUNET_assert (NULL != end); + *end = '\0'; + feedback = json_object_get (sctx->state, + "challenge_feedback"); + if (NULL == feedback) + { + feedback = json_object (); + GNUNET_assert (0 == + json_object_set_new (sctx->state, + "challenge_feedback", + feedback)); + } + switch (csr->cs) + { + case ANASTASIS_CHALLENGE_STATUS_SOLVED: + { + json_t *rd; + + rd = ANASTASIS_recovery_serialize (sctx->r); + if (NULL == rd) + { + GNUNET_break (0); + set_state (sctx->state, + ANASTASIS_GENERIC_STATE_ERROR); + sctx->cb (sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + sctx->state); + sctx_free (sctx); + return; + } + GNUNET_assert (0 == + json_object_set_new (sctx->state, + "recovery_document", + rd)); + } + { + json_t *solved; + + solved = json_pack ("{s:s}", + "state", + "solved"); + GNUNET_assert (NULL != solved); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + solved)); + } + /* Delay reporting challenge success, as we MAY still + also see a secret recovery success (and we can only + call the callback once) */ + sctx->delayed_report = GNUNET_SCHEDULER_add_now (&report_solved, + sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_INSTRUCTIONS: + { + json_t *instructions; + json_t *val; + const char *mime; + + mime = csr->details.open_challenge.content_type; + if (NULL != mime) + { + if ( (0 == strcasecmp (mime, + "text/plain")) || + (0 == strcasecmp (mime, + "text/utf8")) ) + { + char *s = GNUNET_strndup (csr->details.open_challenge.body, + csr->details.open_challenge.body_size); + + instructions = json_pack ( + "{s:s, s:s, s:I}", + "state", + "hint", + "hint", + s, + "http_status", + (json_int_t) csr->details.open_challenge.http_status); + GNUNET_free (s); + } + else if (0 == strcasecmp (mime, + "application/json")) + { + json_t *body; + + body = json_loadb (csr->details.open_challenge.body, + csr->details.open_challenge.body_size, + JSON_REJECT_DUPLICATES, + NULL); + if (NULL == body) + { + GNUNET_break_op (0); + mime = NULL; + } + else + { + instructions = json_pack ( + "{s:s, s:o, s:I}", + "state", + "details", + "details", + body, + "http_status", + (json_int_t) csr->details.open_challenge.http_status); + } + } + else + { + /* unexpected / unsupported mime type */ + mime = NULL; + } + } + if (NULL == mime) + { + val = GNUNET_JSON_from_data (csr->details.open_challenge.body, + csr->details.open_challenge.body_size); + GNUNET_assert (NULL != val); + instructions = json_pack ( + "{s:s, s:o, s:I, s:s?}", + "state", + "body", + "body", + val, + "http_status", + (json_int_t) csr->details.open_challenge.http_status, + "mime_type", + mime); + } + GNUNET_assert (NULL != instructions); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + instructions)); + } + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_REDIRECT_FOR_AUTHENTICATION: + { + json_t *redir; + + redir = json_pack ("{s:s, s:s}", + "state", + "redirect", + "redirect_url", + csr->details.redirect_url); + GNUNET_assert (NULL != redir); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + redir)); + } + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_PAYMENT_REQUIRED: + { + json_t *pay; + + pay = json_pack ("{s:s, s:s, s:s, s:o}", + "state", + "payment", + "taler_pay_uri", + csr->details.payment_required.taler_pay_uri, + "provider", + cd->provider_url, + "payment_secret", + GNUNET_JSON_from_data_auto ( + &csr->details.payment_required.payment_secret)); + GNUNET_assert (NULL != pay); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + pay)); + } + /* Remember payment secret for later (once application claims it paid) */ + { + json_t *challenge = find_challenge_in_ri (sctx->state, + &cd->uuid); + + GNUNET_assert (NULL != challenge); + GNUNET_assert (0 == + json_object_set_new (challenge, + "payment_secret", + GNUNET_JSON_from_data_auto ( + &csr->details.payment_required. + payment_secret))); + } + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_SERVER_FAILURE: + { + json_t *err; + + err = json_pack ("{s:s, s:I, s:I}", + "state", + "server-failure", + "http_status", + (json_int_t) csr->details.server_failure.http_status, + "error_code", + (json_int_t) csr->details.server_failure.ec); + GNUNET_assert (NULL != err); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + err)); + } + GNUNET_break_op (0); + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_TRUTH_UNKNOWN: + { + json_t *err; + + err = json_pack ("{s:s, s:I}", + "state", + "truth-unknown", + "error_code", + (json_int_t) TALER_EC_ANASTASIS_TRUTH_UNKNOWN); + GNUNET_assert (NULL != err); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + err)); + } + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + case ANASTASIS_CHALLENGE_STATUS_RATE_LIMIT_EXCEEDED: + { + json_t *err; + + err = json_pack ("{s:s, s:I}", + "state", + "rate-limit-exceeded", + "error_code", + (json_int_t) TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED); + + GNUNET_assert (NULL != err); + GNUNET_assert (0 == + json_object_set_new (feedback, + uuid, + err)); + } + GNUNET_break_op (0); + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + } + GNUNET_break (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + sctx_free (sctx); +} + + +/** + * Callback which passes back the recovery document and its possible + * policies. Also passes back the version of the document for the user + * to check. + * + * We find the selected challenge and try to answer it (or begin + * the process). + * + * @param cls a `struct SelectChallengeContext *` + * @param ri recovery information struct which contains the policies + */ +static void +solve_challenge_cb (void *cls, + const struct ANASTASIS_RecoveryInformation *ri) +{ + struct SelectChallengeContext *sctx = cls; + const struct ANASTASIS_PaymentSecretP *psp = NULL; + struct ANASTASIS_PaymentSecretP ps; + struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; + struct GNUNET_JSON_Specification tspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &timeout)), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &ps), + GNUNET_JSON_spec_end () + }; + + json_t *challenge; + + if (NULL == ri) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "recovery information could not be deserialized"); + sctx_free (sctx); + return; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (sctx->args, + tspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'timeout' malformed"); + sctx_free (sctx); + return; + } + + /* Check if we got a payment_secret */ + challenge = find_challenge_in_ri (sctx->state, + &sctx->uuid); + if (NULL == challenge) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "challenge not found"); + sctx_free (sctx); + return; + } + + if (NULL != + json_object_get (sctx->args, + "payment_secret")) + { + /* check if we got payment secret in args */ + if (GNUNET_OK != + GNUNET_JSON_parse (sctx->args, + pspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'payment_secret' malformed"); + sctx_free (sctx); + return; + } + psp = &ps; + } + else if (NULL != + json_object_get (challenge, + "payment_secret")) + { + if (GNUNET_OK != + GNUNET_JSON_parse (challenge, + pspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'payment_secret' malformed"); + sctx_free (sctx); + return; + } + psp = &ps; + } + + for (unsigned int i = 0; ics_len; i++) + { + struct ANASTASIS_Challenge *ci = ri->cs[i]; + const struct ANASTASIS_ChallengeDetails *cd; + int ret; + + cd = ANASTASIS_challenge_get_details (ci); + if (0 != + GNUNET_memcmp (&sctx->uuid, + &cd->uuid)) + continue; + if (cd->solved) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "Selected challenge already solved"); + sctx_free (sctx); + return; + } + if (0 == strcmp ("question", + cd->type)) + { + /* security question, answer must be a string */ + json_t *janswer = json_object_get (sctx->args, + "answer"); + const char *answer = json_string_value (janswer); + + if (NULL == answer) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'answer' missing"); + sctx_free (sctx); + return; + } + /* persist answer, in case payment is required */ + GNUNET_assert (0 == + json_object_set (challenge, + "answer", + janswer)); + ret = ANASTASIS_challenge_answer (ci, + psp, + timeout, + answer, + &answer_feedback_cb, + sctx); + } + else + { + /* Check if we got a PIN or a HASH */ + json_t *pin = json_object_get (sctx->args, + "pin"); + json_t *hash = json_object_get (sctx->args, + "hash"); + if (json_is_integer (pin)) + { + uint64_t ianswer = json_integer_value (pin); + + ret = ANASTASIS_challenge_answer2 (ci, + psp, + timeout, + ianswer, + &answer_feedback_cb, + sctx); + } + else if (NULL != hash) + { + struct GNUNET_HashCode hashed_answer; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("hash", + &hashed_answer), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (sctx->args, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'answer' malformed"); + sctx_free (sctx); + return; + } + ret = ANASTASIS_challenge_start (ci, + psp, + timeout, + &hashed_answer, + &answer_feedback_cb, + sctx); + } + else + { + /* no answer provided */ + ret = ANASTASIS_challenge_start (ci, + psp, + timeout, + NULL, /* no answer */ + &answer_feedback_cb, + sctx); + } + } + if (GNUNET_OK != ret) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Failed to begin answering challenge"); + sctx_free (sctx); + return; + } + return; /* await answer feedback */ + } + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'uuid' not in list of challenges"); + sctx_free (sctx); +} + + +/** + * Callback which passes back the recovery document and its possible + * policies. Also passes back the version of the document for the user + * to check. + * + * We find the selected challenge and try to answer it (or begin + * the process). + * + * @param cls a `struct SelectChallengeContext *` + * @param ri recovery information struct which contains the policies + */ +static void +pay_challenge_cb (void *cls, + const struct ANASTASIS_RecoveryInformation *ri) +{ + struct SelectChallengeContext *sctx = cls; + json_t *challenge; + + if (NULL == ri) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "recovery information could not be deserialized"); + sctx_free (sctx); + return; + } + + challenge = find_challenge_in_ri (sctx->state, + &sctx->uuid); + if (NULL == challenge) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "challenge not found"); + sctx_free (sctx); + return; + } + /* persist payment, in case we need to run the request again */ + GNUNET_assert ( + 0 == + json_object_set_new (challenge, + "payment_secret", + GNUNET_JSON_from_data_auto (&sctx->ps))); + + for (unsigned int i = 0; ics_len; i++) + { + struct ANASTASIS_Challenge *ci = ri->cs[i]; + const struct ANASTASIS_ChallengeDetails *cd; + int ret; + + cd = ANASTASIS_challenge_get_details (ci); + if (0 != + GNUNET_memcmp (&sctx->uuid, + &cd->uuid)) + continue; + if (cd->solved) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "Selected challenge already solved"); + sctx_free (sctx); + return; + } + + if (0 == strcmp ("question", + cd->type)) + { + /* security question, answer must be a string and already ready */ + json_t *janswer = json_object_get (challenge, + "answer"); + const char *answer = json_string_value (janswer); + + if (NULL == answer) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'answer' missing"); + sctx_free (sctx); + return; + } + ret = ANASTASIS_challenge_answer (ci, + &sctx->ps, + sctx->timeout, + answer, + &answer_feedback_cb, + sctx); + } + else + { + ret = ANASTASIS_challenge_start (ci, + &sctx->ps, + sctx->timeout, + NULL, /* no answer yet */ + &answer_feedback_cb, + sctx); + } + if (GNUNET_OK != ret) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Failed to begin answering challenge"); + sctx_free (sctx); + return; + } + return; /* await answer feedback */ + } + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'uuid' not in list of challenges"); + sctx_free (sctx); +} + + +/** + * The user selected a challenge to be solved. Begin the solving + * process. + * + * @param[in] state we are in + * @param arguments our arguments with the solution + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return handle to cancel challenge selection step + */ +static struct ANASTASIS_ReduxAction * +solve_challenge (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct SelectChallengeContext *sctx + = GNUNET_new (struct SelectChallengeContext); + json_t *rd; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("selected_challenge_uuid", + &sctx->uuid), + GNUNET_JSON_spec_end () + }; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'selected_challenge_uuid' missing"); + return NULL; + } + rd = json_object_get (state, + "recovery_document"); + if (NULL == rd) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "solve_challenge"); + return NULL; + } + sctx->cb = cb; + sctx->cb_cls = cb_cls; + sctx->state = json_incref (state); + sctx->args = json_incref ((json_t*) arguments); + sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, + rd, + &solve_challenge_cb, + sctx, + &core_secret_cb, + sctx); + if (NULL == sctx->r) + { + json_decref (sctx->state); + json_decref (sctx->args); + GNUNET_free (sctx); + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'recovery_document' invalid"); + return NULL; + } + sctx->ra.cleanup = &sctx_free; + sctx->ra.cleanup_cls = sctx; + return &sctx->ra; +} + + +/** + * The user selected a challenge to be solved. Handle the payment + * process. + * + * @param[in] state we are in + * @param arguments our arguments with the solution + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return handle to cancel challenge selection step + */ +static struct ANASTASIS_ReduxAction * +pay_challenge (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct SelectChallengeContext *sctx + = GNUNET_new (struct SelectChallengeContext); + json_t *rd; + struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("selected_challenge_uuid", + &sctx->uuid), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification aspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &timeout)), + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &sctx->ps), + GNUNET_JSON_spec_end () + }; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (arguments, + aspec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'payment_secret' missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (state, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'selected_challenge_uuid' missing"); + return NULL; + } + rd = json_object_get (state, + "recovery_document"); + if (NULL == rd) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "pay_challenge"); + return NULL; + } + sctx->timeout = timeout; + sctx->cb = cb; + sctx->cb_cls = cb_cls; + sctx->state = json_incref (state); + sctx->args = json_incref ((json_t*) arguments); + sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, + rd, + &pay_challenge_cb, + sctx, + &core_secret_cb, + sctx); + if (NULL == sctx->r) + { + json_decref (sctx->state); + json_decref (sctx->args); + GNUNET_free (sctx); + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'recovery_document' invalid"); + return NULL; + } + sctx->ra.cleanup = &sctx_free; + sctx->ra.cleanup_cls = sctx; + return &sctx->ra; +} + + +/** + * Callback which passes back the recovery document and its possible + * policies. Also passes back the version of the document for the user + * to check. + * + * We find the selected challenge and try to answer it (or begin + * the process). + * + * @param cls a `struct SelectChallengeContext *` + * @param ri recovery information struct which contains the policies + */ +static void +select_challenge_cb (void *cls, + const struct ANASTASIS_RecoveryInformation *ri) +{ + struct SelectChallengeContext *sctx = cls; + const struct ANASTASIS_PaymentSecretP *psp = NULL; + struct ANASTASIS_PaymentSecretP ps; + struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO; + struct GNUNET_JSON_Specification tspec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("timeout", + &timeout)), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification pspec[] = { + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &ps), + GNUNET_JSON_spec_end () + }; + + + if (NULL == ri) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "recovery information could not be deserialized"); + sctx_free (sctx); + return; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (sctx->args, + tspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'timeout' malformed"); + sctx_free (sctx); + return; + } + + /* NOTE: do we need both ways to pass payment secrets? */ + if (NULL != + json_object_get (sctx->args, + "payment_secret")) + { + /* check if we got payment secret in args */ + if (GNUNET_OK != + GNUNET_JSON_parse (sctx->args, + pspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'payment_secret' malformed"); + sctx_free (sctx); + return; + } + psp = &ps; + } + else + { + /* Check if we got a payment_secret in state */ + json_t *challenge = find_challenge_in_ri (sctx->state, + &sctx->uuid); + + if (NULL == challenge) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "challenge not found"); + sctx_free (sctx); + return; + } + if (NULL != + json_object_get (challenge, + "payment_secret")) + { + if (GNUNET_OK != + GNUNET_JSON_parse (challenge, + pspec, + NULL, NULL)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'payment_secret' malformed"); + sctx_free (sctx); + return; + } + psp = &ps; + } + } + + for (unsigned int i = 0; ics_len; i++) + { + struct ANASTASIS_Challenge *ci = ri->cs[i]; + const struct ANASTASIS_ChallengeDetails *cd; + int ret; + + cd = ANASTASIS_challenge_get_details (ci); + if (0 != + GNUNET_memcmp (&sctx->uuid, + &cd->uuid)) + continue; + if (cd->solved) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "Selected challenge already solved"); + sctx_free (sctx); + return; + } + GNUNET_assert ( + 0 == + json_object_set_new (sctx->state, + "selected_challenge_uuid", + GNUNET_JSON_from_data_auto (&cd->uuid))); + if (0 == strcmp ("question", + cd->type)) + { + /* security question, immediately request user to answer it */ + set_state (sctx->state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); + sctx->cb (sctx->cb_cls, + TALER_EC_NONE, + sctx->state); + sctx_free (sctx); + return; + } + /* trigger challenge */ + ret = ANASTASIS_challenge_start (ci, + psp, + timeout, + NULL, /* no answer */ + &answer_feedback_cb, + sctx); + if (GNUNET_OK != ret) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Failed to begin answering challenge"); + sctx_free (sctx); + return; + } + return; /* await answer feedback */ + } + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'uuid' not in list of challenges"); + sctx_free (sctx); +} + + +/** + * The user selected a challenge to be solved. Begin the solving + * process. + * + * @param[in] state we are in + * @param arguments our arguments with the solution + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return handle to cancel challenge selection step + */ +static struct ANASTASIS_ReduxAction * +select_challenge (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct SelectChallengeContext *sctx + = GNUNET_new (struct SelectChallengeContext); + json_t *rd; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("uuid", + &sctx->uuid), + GNUNET_JSON_spec_end () + }; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + if (GNUNET_OK != + GNUNET_JSON_parse (arguments, + spec, + NULL, NULL)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'uuid' missing"); + return NULL; + } + rd = json_object_get (state, + "recovery_document"); + if (NULL == rd) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "select_challenge"); + return NULL; + } + sctx->cb = cb; + sctx->cb_cls = cb_cls; + sctx->state = json_incref (state); + sctx->args = json_incref ((json_t*) arguments); + sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, + rd, + &select_challenge_cb, + sctx, + &core_secret_cb, + sctx); + if (NULL == sctx->r) + { + json_decref (sctx->state); + json_decref (sctx->args); + GNUNET_free (sctx); + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'recovery_document' invalid"); + return NULL; + } + sctx->ra.cleanup = &sctx_free; + sctx->ra.cleanup_cls = sctx; + return &sctx->ra; +} + + +/** + * The user pressed "back" during challenge solving. + * Transition back to selecting another challenge. + * + * @param[in] state we are in + * @param arguments our arguments (unused) + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return NULL (synchronous operation) + */ +static struct ANASTASIS_ReduxAction * +back_challenge_solving (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + (void) arguments; + GNUNET_assert (0 == + json_object_del (state, + "selected_challenge_uuid")); + set_state (state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * The user wants us to change the policy version. Download another version. + * + * @param[in] state we are in + * @param arguments our arguments with the solution + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return handle to cancel challenge selection step + */ +static struct ANASTASIS_ReduxAction * +change_version (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + uint64_t version; + const char *provider_url; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint64 ("version", + &version), + GNUNET_JSON_spec_string ("provider_url", + &provider_url), + GNUNET_JSON_spec_end () + }; + json_t *ia; + json_t *args; + struct ANASTASIS_ReduxAction *ra; + + if (GNUNET_OK != + GNUNET_JSON_parse (arguments, + spec, + NULL, NULL)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'version' invalid"); + return NULL; + } + GNUNET_assert (NULL != provider_url); + ia = json_object_get (state, + "identity_attributes"); + if (NULL == ia) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'identity_attributes' missing"); + return NULL; + } + args = json_pack ("{s:I, s:O, s:s}", + "version", + (json_int_t) version, + "identity_attributes", + ia, + "provider_url", + provider_url); + if (NULL == args) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + NULL); + return NULL; + } + ra = ANASTASIS_REDUX_recovery_challenge_begin_ (state, + args, + cb, + cb_cls); + json_decref (args); + return ra; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "next" action in "secret_selecting" state. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +done_secret_selecting (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *ri; + + ri = json_object_get (state, + "recovery_information"); + if ( (NULL == ri) || + (NULL == json_object_get (ri, + "challenges")) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE, + "no valid version selected"); + return NULL; + } + set_state (state, + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * Signature of callback function that implements a state transition. + * + * @param state current state + * @param arguments arguments for the state transition + * @param cb function to call when done + * @param cb_cls closure for @a cb + */ +typedef struct ANASTASIS_ReduxAction * +(*DispatchHandler)(json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * 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[in,out] state input/output state (to be modified) + * @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 + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_recovery_action_ (json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct Dispatcher + { + enum ANASTASIS_RecoveryState recovery_state; + const char *recovery_action; + DispatchHandler fun; + } dispatchers[] = { + { + ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, + "change_version", + &change_version + }, + { + ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, + "next", + &done_secret_selecting + }, + { + ANASTASIS_RECOVERY_STATE_SECRET_SELECTING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING, + "select_challenge", + &select_challenge + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING, + "pay", + &pay_challenge + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING, + "solve_challenge", + &solve_challenge + }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING, + "back", + &back_challenge_solving + }, + { ANASTASIS_RECOVERY_STATE_ERROR, NULL, NULL } + }; + const char *s = json_string_value (json_object_get (state, + "recovery_state")); + enum ANASTASIS_RecoveryState rs; + + GNUNET_assert (NULL != s); + rs = ANASTASIS_recovery_state_from_string_ (s); + if (ANASTASIS_RECOVERY_STATE_ERROR == rs) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'recovery_state' field invalid"); + return NULL; + } + for (unsigned int i = 0; NULL != dispatchers[i].fun; i++) + { + if ( (rs == dispatchers[i].recovery_state) && + (0 == strcmp (action, + dispatchers[i].recovery_action)) ) + { + return dispatchers[i].fun (state, + arguments, + cb, + cb_cls); + } + } + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID, + action); + return NULL; +} + + +/** + * State for a "recover secret" CMD. + */ +struct RecoverSecretState; + + +/** + * State for a "policy download" as part of a recovery operation. + */ +struct PolicyDownloadEntry +{ + + /** + * Kept in a DLL. + */ + struct PolicyDownloadEntry *prev; + + /** + * Kept in a DLL. + */ + struct PolicyDownloadEntry *next; + + /** + * Backend we are querying. + */ + char *backend_url; + + /** + * Salt to be used to derive the id for this provider + */ + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + + /** + * Context we operate in. + */ + struct RecoverSecretState *rss; + + /** + * The /policy GET operation handle. + */ + struct ANASTASIS_Recovery *recovery; + +}; + + +/** + * Entry in the list of all known applicable Anastasis providers. + * Used to wait for it to complete downloading /config. + */ +struct RecoveryStartStateProviderEntry +{ + /** + * Kept in a DLL. + */ + struct RecoveryStartStateProviderEntry *next; + + /** + * Kept in a DLL. + */ + struct RecoveryStartStateProviderEntry *prev; + + /** + * Main operation this entry is part of. + */ + struct RecoverSecretState *rss; + + /** + * Resulting provider information, NULL if not (yet) available. + */ + json_t *istate; + + /** + * Ongoing reducer action to obtain /config, NULL if completed. + */ + struct ANASTASIS_ReduxAction *ra; + + /** + * Final result of the operation (once completed). + */ + enum TALER_ErrorCode ec; +}; + + +/** + * State for a "recover secret" CMD. + */ +struct RecoverSecretState +{ + + /** + * Redux action handle associated with this state. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * Head of list of provider /config operations we are doing. + */ + struct RecoveryStartStateProviderEntry *pe_head; + + /** + * Tail of list of provider /config operations we are doing. + */ + struct RecoveryStartStateProviderEntry *pe_tail; + + /** + * Identification data from the user + */ + json_t *id_data; + + /** + * Head of DLL of policy downloads. + */ + struct PolicyDownloadEntry *pd_head; + + /** + * Tail of DLL of policy downloads. + */ + struct PolicyDownloadEntry *pd_tail; + + /** + * Reference to our state. + */ + json_t *state; + + /** + * callback to call during/after operation + */ + ANASTASIS_ActionCallback cb; + + /** + * closure for action callback @e cb. + */ + void *cb_cls; + + /** + * Set if recovery must be done with this provider. + */ + char *provider_url; + + /** + * version of the recovery document to request. + */ + unsigned int version; + + /** + * Number of provider /config operations in @e ba_head that + * are still awaiting completion. + */ + unsigned int pending; + + /** + * Is @e version set? + */ + bool have_version; +}; + + +/** + * Function to free a #RecoverSecretState. + * + * @param cls closure for a #RecoverSecretState. + */ +static void +free_rss (void *cls) +{ + struct RecoverSecretState *rss = cls; + struct PolicyDownloadEntry *pd; + struct RecoveryStartStateProviderEntry *pe; + + while (NULL != (pe = rss->pe_head)) + { + GNUNET_CONTAINER_DLL_remove (rss->pe_head, + rss->pe_tail, + pe); + ANASTASIS_redux_action_cancel (pe->ra); + rss->pending--; + GNUNET_free (pe); + } + while (NULL != (pd = rss->pd_head)) + { + GNUNET_CONTAINER_DLL_remove (rss->pd_head, + rss->pd_tail, + pd); + if (NULL != pd->recovery) + { + ANASTASIS_recovery_abort (pd->recovery); + pd->recovery = NULL; + } + GNUNET_free (pd->backend_url); + GNUNET_free (pd); + } + json_decref (rss->state); + json_decref (rss->id_data); + GNUNET_assert (0 == rss->pending); + GNUNET_free (rss->provider_url); + GNUNET_free (rss); +} + + +/** + * This function is called whenever the recovery process ends. + * In this case, that should not be possible as this callback + * is used before we even begin with the challenges. So if + * we are called, it is because of some fatal error. + * + * @param cls a `struct PolicyDownloadEntry` + * @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 + */ +static void +core_early_secret_cb (void *cls, + enum ANASTASIS_RecoveryStatus rc, + const void *secret, + size_t secret_size) +{ + struct PolicyDownloadEntry *pd = cls; + struct RecoverSecretState *rss = pd->rss; + enum TALER_ErrorCode ec; + + pd->recovery = NULL; + GNUNET_assert (NULL == secret); + GNUNET_CONTAINER_DLL_remove (rss->pd_head, + rss->pd_tail, + pd); + GNUNET_free (pd->backend_url); + GNUNET_free (pd); + if (NULL != rss->pd_head) + return; /* wait for another one */ + /* all failed! report failure! */ + GNUNET_assert (ANASTASIS_RS_SUCCESS != rc); + ec = update_state_by_error (rss->state, + rc); + rss->cb (rss->cb_cls, + ec, + rss->state); + rss->cb = NULL; + free_rss (rss); +} + + +/** + * Determine recovery @a cost of solving a challenge of type @a type + * at @a provider_url by inspecting @a state. + * + * @param state the state to inspect + * @param provider_url the provider to lookup config info from + * @param type the method to lookup the cost of + * @param[out] cost the recovery cost to return + * @return #GNUNET_OK on success, #GNUNET_NO if not found, #GNUNET_SYSERR on state error + */ +static int +lookup_cost (const json_t *state, + const char *provider_url, + const char *type, + struct TALER_Amount *cost) +{ + const json_t *providers; + const json_t *provider; + const json_t *methods; + + providers = json_object_get (state, + "authentication_providers"); + if (NULL == providers) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + provider = json_object_get (providers, + provider_url); + if (NULL == provider) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + methods = json_object_get (provider, + "methods"); + if ( (NULL == methods) || + (! json_is_array (methods)) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + { + size_t index; + json_t *method; + + json_array_foreach (methods, index, method) { + const char *t; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &t), + TALER_JSON_spec_amount_any ("usage_fee", + cost), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (method, + spec, + NULL, NULL)) + { + GNUNET_break (0); + continue; + } + if (0 == strcmp (t, + type)) + return GNUNET_OK; + } + } + return GNUNET_NO; /* not found */ +} + + +/** + * We failed to download a policy. Show an error to the user and + * allow the user to specify alternative providers and/or policy + * versions. + * + * @param[in] rss state to fail with the policy download + * @param offline true of the reason to show is that all providers + * were offline / did not return a salt to us + */ +static void +return_no_policy (struct RecoverSecretState *rss, + bool offline) +{ + json_t *msg; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No provider online, need user to manually specify providers!\n"); + msg = json_pack ("{s:s, s:b}", + "hint", + offline ? "could not contact provider" : + "provider does not know you", + "offline", + offline); + GNUNET_assert (0 == + json_object_set_new (rss->state, + "recovery_error", + msg)); + /* In case there are old ones, remove them! */ + (void) json_object_del (rss->state, + "recovery_document"); + (void) json_object_del (rss->state, + "recovery_information"); + set_state (rss->state, + ANASTASIS_RECOVERY_STATE_SECRET_SELECTING); + rss->cb (rss->cb_cls, + TALER_EC_NONE, + rss->state); + free_rss (rss); +} + + +/** + * Callback which passes back the recovery document and its possible + * policies. Also passes back the version of the document for the user + * to check. + * + * Once the first policy lookup succeeds, we update our state and + * cancel all of the others, passing the obtained recovery information + * back to the user. + * + * @param cls closure for the callback + * @param ri recovery information struct which contains the policies + */ +static void +policy_lookup_cb (void *cls, + const struct ANASTASIS_RecoveryInformation *ri) +{ + struct PolicyDownloadEntry *pd = cls; + struct RecoverSecretState *rss = pd->rss; + json_t *policies; + json_t *challenges; + json_t *recovery_information; + + if (NULL == ri) + { + /* Woopsie, failed hard. */ + GNUNET_CONTAINER_DLL_remove (rss->pd_head, + rss->pd_tail, + pd); + ANASTASIS_recovery_abort (pd->recovery); + GNUNET_free (pd->backend_url); + GNUNET_free (pd); + if (NULL != rss->pd_head) + return; /* wait for another one */ + /* all failed! report failure! */ + return_no_policy (rss, + false); + return; + } + policies = json_array (); + GNUNET_assert (NULL != policies); + for (unsigned int i = 0; idps_len; i++) + { + struct ANASTASIS_DecryptionPolicy *dps = ri->dps[i]; + json_t *pchallenges; + + pchallenges = json_array (); + GNUNET_assert (NULL != pchallenges); + for (unsigned int j = 0; jchallenges_length; j++) + { + struct ANASTASIS_Challenge *c = dps->challenges[j]; + const struct ANASTASIS_ChallengeDetails *cd; + json_t *cj; + + cd = ANASTASIS_challenge_get_details (c); + cj = json_pack ("{s:o}", + "uuid", + GNUNET_JSON_from_data_auto (&cd->uuid)); + GNUNET_assert (NULL != cj); + GNUNET_assert (0 == + json_array_append_new (pchallenges, + cj)); + + } + GNUNET_assert (0 == + json_array_append_new (policies, + pchallenges)); + } /* end for all policies */ + challenges = json_array (); + GNUNET_assert (NULL != challenges); + for (unsigned int i = 0; ics_len; i++) + { + struct ANASTASIS_Challenge *c = ri->cs[i]; + const struct ANASTASIS_ChallengeDetails *cd; + json_t *cj; + struct TALER_Amount cost; + int ret; + + cd = ANASTASIS_challenge_get_details (c); + ret = lookup_cost (rss->state, + cd->provider_url, + cd->type, + &cost); + if (GNUNET_SYSERR == ret) + { + json_decref (challenges); + json_decref (policies); + set_state (rss->state, + ANASTASIS_GENERIC_STATE_ERROR); + ANASTASIS_redux_fail_ (rss->cb, + rss->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "failed to 'lookup_cost'"); + free_rss (rss); + return; + } + + cj = json_pack ("{s:o,s:o?,s:s,s:s}", + "uuid", + GNUNET_JSON_from_data_auto (&cd->uuid), + "cost", + (GNUNET_NO == ret) + ? NULL + : TALER_JSON_from_amount (&cost), + "type", + cd->type, + "instructions", + cd->instructions); + GNUNET_assert (NULL != cj); + GNUNET_assert (0 == + json_array_append_new (challenges, + cj)); + } /* end for all challenges */ + recovery_information = json_pack ("{s:o, s:o, s:s?, s:s, s:I}", + "challenges", challenges, + "policies", policies, + "secret_name", ri->secret_name, + "provider_url", pd->backend_url, + "version", (json_int_t) ri->version); + GNUNET_assert (NULL != recovery_information); + GNUNET_assert (0 == + json_object_set_new (rss->state, + "recovery_information", + recovery_information)); + { + json_t *rd; + + rd = ANASTASIS_recovery_serialize (pd->recovery); + if (NULL == rd) + { + GNUNET_break (0); + set_state (rss->state, + ANASTASIS_GENERIC_STATE_ERROR); + rss->cb (rss->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + rss->state); + free_rss (rss); + return; + } + GNUNET_assert (0 == + json_object_set_new (rss->state, + "recovery_document", + rd)); + } + /* In case there is an old error remove it! */ + (void) json_object_del (rss->state, + "recovery_error"); + set_state (rss->state, + ANASTASIS_RECOVERY_STATE_SECRET_SELECTING); + rss->cb (rss->cb_cls, + TALER_EC_NONE, + rss->state); + free_rss (rss); +} + + +/** + * Try to launch recovery at provider @a provider_url with config @a p_cfg. + * + * @param[in,out] rss recovery context + * @param provider_url base URL of the provider to try + * @param p_cfg configuration of the provider + * @return true if a recovery was launched + */ +static bool +launch_recovery (struct RecoverSecretState *rss, + const char *provider_url, + const json_t *p_cfg) +{ + struct PolicyDownloadEntry *pd = GNUNET_new (struct PolicyDownloadEntry); + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("salt", + &pd->salt), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (p_cfg, + spec, + NULL, NULL)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No salt for `%s', provider offline?\n", + provider_url); + GNUNET_free (pd); + return false; + } + pd->backend_url = GNUNET_strdup (provider_url); + pd->rss = rss; + pd->recovery = ANASTASIS_recovery_begin (ANASTASIS_REDUX_ctx_, + rss->id_data, + rss->have_version + ? rss->version + : 0, + pd->backend_url, + &pd->salt, + &policy_lookup_cb, + pd, + &core_early_secret_cb, + pd); + if (NULL != pd->recovery) + { + GNUNET_CONTAINER_DLL_insert (rss->pd_head, + rss->pd_tail, + pd); + return true; + } + GNUNET_free (pd->backend_url); + GNUNET_free (pd); + return false; +} + + +/** + * We finished downloading /config from all providers, merge + * into the main state, trigger the continuation and free our + * state. + * + * @param[in] rss main state to merge into + */ +static void +providers_complete (struct RecoverSecretState *rss) +{ + bool launched = false; + struct RecoveryStartStateProviderEntry *pe; + json_t *tlist; + + tlist = json_object_get (rss->state, + "authentication_providers"); + if (NULL == tlist) + { + tlist = json_object (); + GNUNET_assert (NULL != tlist); + GNUNET_assert (0 == + json_object_set_new (rss->state, + "authentication_providers", + tlist)); + } + while (NULL != (pe = rss->pe_head)) + { + json_t *provider_list; + + GNUNET_CONTAINER_DLL_remove (rss->pe_head, + rss->pe_tail, + pe); + provider_list = json_object_get (pe->istate, + "authentication_providers"); + /* merge provider_list into tlist (overriding existing entries) */ + if (NULL != provider_list) + { + const char *url; + json_t *value; + + json_object_foreach (provider_list, url, value) { + GNUNET_assert (0 == + json_object_set (tlist, + url, + value)); + } + } + json_decref (pe->istate); + GNUNET_free (pe); + } + + /* now iterate over providers and begin downloading */ + if (NULL != rss->provider_url) + { + json_t *p_cfg; + + p_cfg = json_object_get (tlist, + rss->provider_url); + if (NULL != p_cfg) + launched = launch_recovery (rss, + rss->provider_url, + p_cfg); + } + else + { + json_t *p_cfg; + const char *provider_url; + + json_object_foreach (tlist, provider_url, p_cfg) + { + launched |= launch_recovery (rss, + provider_url, + p_cfg); + } + } + if (! launched) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No provider online, need user to specify different provider!\n"); + return_no_policy (rss, + true); + return; + } +} + + +/** + * Function called when the complete information about a provider + * was added to @a new_state. + * + * @param cls a `struct RecoveryStartStateProviderEntry` + * @param error error code + * @param new_state resulting new state + */ +static void +provider_added_cb (void *cls, + enum TALER_ErrorCode error, + json_t *new_state) +{ + struct RecoveryStartStateProviderEntry *pe = cls; + + pe->ra = NULL; + pe->istate = json_incref (new_state); + pe->ec = error; + pe->rss->pending--; + if (0 == pe->rss->pending) + providers_complete (pe->rss); +} + + +/** + * Start to query provider for recovery document. + * + * @param[in,out] rss overall recovery state + * @param provider_url base URL of the provider to query + */ +static void +begin_query_provider (struct RecoverSecretState *rss, + const char *provider_url) +{ + struct RecoveryStartStateProviderEntry *pe; + json_t *istate; + + pe = GNUNET_new (struct RecoveryStartStateProviderEntry); + pe->rss = rss; + istate = json_object (); + GNUNET_assert (NULL != istate); + GNUNET_CONTAINER_DLL_insert (rss->pe_head, + rss->pe_tail, + pe); + pe->ra = ANASTASIS_REDUX_add_provider_to_state_ (provider_url, + istate, + &provider_added_cb, + pe); + json_decref (istate); + if (NULL != pe->ra) + rss->pending++; +} + + +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_recovery_challenge_begin_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *version; + json_t *providers; + const json_t *attributes; + struct RecoverSecretState *rss; + const char *provider_url; + + providers = json_object_get (state, + "authentication_providers"); + if ( (NULL == providers) || + (! json_is_object (providers)) ) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'authentication_providers' missing"); + return NULL; + } + attributes = json_object_get (arguments, + "identity_attributes"); + if ( (NULL == attributes) || + (! json_is_object (attributes)) ) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'identity_attributes' missing"); + return NULL; + } + rss = GNUNET_new (struct RecoverSecretState); + rss->id_data = json_incref ((json_t *) attributes); + version = json_object_get (arguments, + "version"); + if (NULL != version) + { + rss->version = (unsigned int) json_integer_value (version); + rss->have_version = true; + } + rss->state = json_incref (state); + rss->cb = cb; + rss->cb_cls = cb_cls; + rss->pending = 1; /* decremented after initialization loop */ + + provider_url = json_string_value (json_object_get (arguments, + "provider_url")); + if (NULL != provider_url) + { + rss->provider_url = GNUNET_strdup (provider_url); + begin_query_provider (rss, + provider_url); + } + else + { + json_t *prov; + const char *url; + + json_object_foreach (providers, url, prov) { + begin_query_provider (rss, + url); + } + } + rss->pending--; + if (0 == rss->pending) + { + providers_complete (rss); + if (NULL == rss->cb) + return NULL; + } + rss->ra.cleanup = &free_rss; + rss->ra.cleanup_cls = rss; + return &rss->ra; +} diff --git a/src/reducer/anastasis_api_redux.c b/src/reducer/anastasis_api_redux.c new file mode 100644 index 0000000..eb7e362 --- /dev/null +++ b/src/reducer/anastasis_api_redux.c @@ -0,0 +1,1730 @@ +/* + 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 +*/ +/** + * @file lib/anastasis_api_redux.c + * @brief anastasis reducer api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include +#include +#include "anastasis_redux.h" +#include "anastasis_error_codes.h" +#include +#include "anastasis_api_redux.h" +#include + + +/** + * How long do we wait at most for a /config reply from an Anastasis provider. + * 60s is very generous, given the tiny bandwidth required, even for the most + * remote locations. + */ +#define CONFIG_GENERIC_TIMEOUT GNUNET_TIME_UNIT_MINUTES + + +#define GENERATE_STRING(STRING) #STRING, +static const char *generic_strings[] = { + ANASTASIS_GENERIC_STATES (GENERATE_STRING) +}; +#undef GENERATE_STRING + + +/** + * #ANASTASIS_REDUX_add_provider_to_state_ waiting for the + * configuration request to complete or fail. + */ +struct ConfigReduxWaiting +{ + /** + * Kept in a DLL. + */ + struct ConfigReduxWaiting *prev; + + /** + * Kept in a DLL. + */ + struct ConfigReduxWaiting *next; + + /** + * Associated redux action. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * Config request we are waiting for. + */ + struct ConfigRequest *cr; + + /** + * State we are processing. + */ + json_t *state; + + /** + * Function to call with updated @e state. + */ + ANASTASIS_ActionCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + +}; + + +/** + * Anastasis authorization method configuration + */ +struct AuthorizationMethodConfig +{ + /** + * Type of the method, i.e. "question". + */ + char *type; + + /** + * Fee charged for accessing key share using this method. + */ + struct TALER_Amount usage_fee; +}; + + +/** + * State for a "get config" operation. + */ +struct ConfigRequest +{ + + /** + * Kept in a DLL, given that we may have multiple backends. + */ + struct ConfigRequest *next; + + /** + * Kept in a DLL, given that we may have multiple backends. + */ + struct ConfigRequest *prev; + + /** + * Head of DLL of REDUX operations waiting for an answer. + */ + struct ConfigReduxWaiting *w_head; + + /** + * Tail of DLL of REDUX operations waiting for an answer. + */ + struct ConfigReduxWaiting *w_tail; + + /** + * Obtained status code. + */ + unsigned int http_status; + + /** + * The /config GET operation handle. + */ + struct ANASTASIS_ConfigOperation *co; + + /** + * URL of the anastasis backend. + */ + char *url; + + /** + * Business name of the anastasis backend. + */ + char *business_name; + + /** + * currency used by the anastasis backend. + */ + char *currency; + + /** + * Array of authorization methods supported by the server. + */ + struct 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; + + /** + * Task to timeout /config requests. + */ + struct GNUNET_SCHEDULER_Task *tt; + + /** + * Status of the /config request. + */ + enum TALER_ErrorCode ec; +}; + + +/** + * Reducer API's CURL context handle. + */ +struct GNUNET_CURL_Context *ANASTASIS_REDUX_ctx_; + +/** + * JSON containing country specific identity attributes to ask the user for. + */ +static json_t *redux_id_attr; + +/** + * Head of DLL of Anastasis backend configuration requests. + */ +static struct ConfigRequest *cr_head; + +/** + * Tail of DLL of Anastasis backend configuration requests. + */ +static struct ConfigRequest *cr_tail; + +/** + * JSON containing country specific information. + */ +static json_t *redux_countries; + +/** + * List of Anastasis providers. + */ +static json_t *provider_list; + + +/** + * Extract the mode of a state from json + * + * @param state the state to operate on + * @return "backup_state" or "recovery_state" + */ +static const char * +get_state_mode (const json_t *state) +{ + if (json_object_get (state, "backup_state")) + return "backup_state"; + if (json_object_get (state, "recovery_state")) + return "recovery_state"; + GNUNET_assert (0); + return NULL; +} + + +enum ANASTASIS_GenericState +ANASTASIS_generic_state_from_string_ (const char *state_string) +{ + for (enum ANASTASIS_GenericState i = 0; + i < sizeof (generic_strings) / sizeof(*generic_strings); + i++) + if (0 == strcmp (state_string, + generic_strings[i])) + return i; + return ANASTASIS_GENERIC_STATE_ERROR; +} + + +const char * +ANASTASIS_generic_state_to_string_ (enum ANASTASIS_GenericState gs) +{ + if ( (gs < 0) || + (gs >= sizeof (generic_strings) / sizeof(*generic_strings)) ) + { + GNUNET_break_op (0); + return NULL; + } + return generic_strings[gs]; +} + + +void +ANASTASIS_redux_fail_ (ANASTASIS_ActionCallback cb, + void *cb_cls, + enum TALER_ErrorCode ec, + const char *detail) +{ + json_t *estate; + + estate = json_pack ("{s:s?, s:I, s:s}", + "detail", detail, + "code", (json_int_t) ec, + "hint", TALER_ErrorCode_get_hint (ec)); + cb (cb_cls, + ec, + estate); + json_decref (estate); +} + + +/** + * Transition the @a state to @a gs. + * + * @param[in,out] state to transition + * @param gs state to transition to + */ +static void +redux_transition (json_t *state, + enum ANASTASIS_GenericState gs) +{ + const char *s_mode = get_state_mode (state); + + GNUNET_assert (0 == + json_object_set_new ( + state, + s_mode, + json_string ( + ANASTASIS_generic_state_to_string_ (gs)))); + +} + + +void +ANASTASIS_redux_init (struct GNUNET_CURL_Context *ctx) +{ + ANASTASIS_REDUX_ctx_ = ctx; +} + + +/** + * Function to free a #ConfigRequest, an async operation. + * + * @param cr state for a "get config" operation + */ +static void +free_config_request (struct ConfigRequest *cr) +{ + GNUNET_assert (NULL == cr->w_head); + if (NULL != cr->co) + ANASTASIS_config_cancel (cr->co); + if (NULL != cr->tt) + GNUNET_SCHEDULER_cancel (cr->tt); + GNUNET_free (cr->currency); + GNUNET_free (cr->url); + GNUNET_free (cr->business_name); + for (unsigned int i = 0; imethods_length; i++) + GNUNET_free (cr->methods[i].type); + GNUNET_free (cr->methods); + GNUNET_free (cr); +} + + +void +ANASTASIS_redux_done () +{ + struct ConfigRequest *cr; + + while (NULL != (cr = cr_head)) + { + GNUNET_CONTAINER_DLL_remove (cr_head, + cr_tail, + cr); + free_config_request (cr); + } + ANASTASIS_REDUX_ctx_ = NULL; + if (NULL != redux_countries) + { + json_decref (redux_countries); + redux_countries = NULL; + } + if (NULL != redux_id_attr) + { + json_decref (redux_id_attr); + redux_id_attr = NULL; + } + if (NULL != provider_list) + { + json_decref (provider_list); + provider_list = NULL; + } +} + + +const json_t * +ANASTASIS_redux_countries_init_ (void) +{ + char *dn; + json_error_t error; + + if (NULL != redux_countries) + return redux_countries; + + { + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return NULL; + } + GNUNET_asprintf (&dn, + "%s/redux.countries.json", + path); + GNUNET_free (path); + } + redux_countries = json_load_file (dn, + JSON_COMPACT, + &error); + if (NULL == redux_countries) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse `%s': %s at %d:%d (%d)\n", + dn, + error.text, + error.line, + error.column, + error.position); + GNUNET_free (dn); + return NULL; + } + GNUNET_free (dn); + return redux_countries; +} + + +/** + * Abort waiting for /config reply. + * + * @param cls a `struct ConfigReduxWaiting` handle. + */ +static void +abort_provider_config_cb (void *cls) +{ + struct ConfigReduxWaiting *w = cls; + struct ConfigRequest *cr = w->cr; + + GNUNET_CONTAINER_DLL_remove (cr->w_head, + cr->w_tail, + w); + json_decref (w->state); + GNUNET_free (w); +} + + +/** + * Notify anyone waiting on @a cr that the request is done + * (successful or failed). + * + * @param[in,out] cr request that completed + */ +static void +notify_waiting (struct ConfigRequest *cr) +{ + struct ConfigReduxWaiting *w; + + while (NULL != (w = cr->w_head)) + { + json_t *provider_list; + json_t *prov; + + if (NULL == (provider_list = json_object_get (w->state, + "authentication_providers"))) + { + GNUNET_assert (0 == + json_object_set_new (w->state, + "authentication_providers", + provider_list = json_object ())); + } + provider_list = json_object_get (w->state, + "authentication_providers"); + GNUNET_assert (NULL != provider_list); + + if (TALER_EC_NONE != cr->ec) + { + prov = json_pack ("{s:I, s:I}", + "error_code", + (json_int_t) cr->ec, + "http_status", + (json_int_t) cr->http_status); + } + else + { + json_t *methods_list; + + methods_list = json_array (); + GNUNET_assert (NULL != methods_list); + for (unsigned int i = 0; imethods_length; i++) + { + struct AuthorizationMethodConfig *method = &cr->methods[i]; + json_t *mj = json_pack ("{s:s, s:o}", + "type", + method->type, + "usage_fee", + TALER_JSON_from_amount (&method->usage_fee)); + + GNUNET_assert (NULL != mj); + GNUNET_assert (0 == + json_array_append_new (methods_list, + mj)); + } + prov = json_pack ("{s:o, s:o, s:o, s:o, s:s," + " s:s, s:I, s:o, s:I}", + "methods", + methods_list, + "annual_fee", + TALER_JSON_from_amount (&cr->annual_fee), + "truth_upload_fee", + TALER_JSON_from_amount (&cr->truth_upload_fee), + "liability_limit", + TALER_JSON_from_amount (&cr->liability_limit), + "currency", + cr->currency, + /* 6 */ + "business_name", + cr->business_name, + "storage_limit_in_megabytes", + (json_int_t) cr->storage_limit_in_megabytes, + "salt", + GNUNET_JSON_from_data_auto (&cr->salt), + "http_status", + (json_int_t) cr->http_status); + } + GNUNET_assert (0 == + json_object_set_new (provider_list, + cr->url, + prov)); + w->cb (w->cb_cls, + cr->ec, + w->state); + abort_provider_config_cb (w); + } + +} + + +/** + * Function called with the results of a #ANASTASIS_get_config(). + * + * @param cls closure + * @param http_status HTTP status of the request + * @param acfg anastasis configuration + */ +static void +config_cb (void *cls, + unsigned int http_status, + const struct ANASTASIS_Config *acfg) +{ + struct ConfigRequest *cr = cls; + + cr->co = NULL; + GNUNET_SCHEDULER_cancel (cr->tt); + cr->tt = NULL; + cr->http_status = http_status; + if (MHD_HTTP_OK != http_status) + cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED; + if ( (MHD_HTTP_OK == http_status) && + (NULL == acfg) ) + { + cr->http_status = MHD_HTTP_NOT_FOUND; + cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED; + } + else if (NULL != acfg) + { + if (0 == acfg->storage_limit_in_megabytes) + { + cr->http_status = 0; + cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG; + } + else + { + cr->currency = GNUNET_strdup (acfg->currency); + cr->business_name = GNUNET_strdup (acfg->business_name); + cr->methods = GNUNET_new_array (acfg->methods_length, + struct AuthorizationMethodConfig); + for (unsigned int i = 0; imethods_length; i++) + { + cr->methods[i].type = GNUNET_strdup (acfg->methods[i].type); + cr->methods[i].usage_fee = acfg->methods[i].usage_fee; + } + cr->methods_length = acfg->methods_length; + cr->storage_limit_in_megabytes = acfg->storage_limit_in_megabytes; + cr->annual_fee = acfg->annual_fee; + cr->truth_upload_fee = acfg->truth_upload_fee; + cr->liability_limit = acfg->liability_limit; + cr->salt = acfg->salt; + } + } + notify_waiting (cr); +} + + +/** + * Aborts a "get config" after timeout. + * + * @param cls closure for a "get config" request + */ +static void +config_request_timeout (void *cls) +{ + struct ConfigRequest *cr = cls; + + cr->tt = NULL; + ANASTASIS_config_cancel (cr->co); + cr->co = NULL; + cr->http_status = 0; + cr->ec = TALER_EC_GENERIC_TIMEOUT; + notify_waiting (cr); +} + + +/** + * Schedule job to obtain Anastasis provider configuration at @a url. + * + * @param url base URL of Anastasis provider + * @return check config handle + */ +static struct ConfigRequest * +check_config (const char *url) +{ + struct ConfigRequest *cr; + + for (cr = cr_head; NULL != cr; cr = cr->next) + { + if (0 != strcmp (url, + cr->url)) + continue; + if (NULL != cr->co) + return cr; /* already on it */ + break; + } + if (NULL == cr) + { + cr = GNUNET_new (struct ConfigRequest); + cr->url = GNUNET_strdup (url); + GNUNET_CONTAINER_DLL_insert (cr_head, + cr_tail, + cr); + } + cr->co = ANASTASIS_get_config (ANASTASIS_REDUX_ctx_, + cr->url, + &config_cb, + cr); + if (NULL == cr->co) + { + GNUNET_break (0); + return NULL; + } + else + { + cr->tt = GNUNET_SCHEDULER_add_delayed (CONFIG_GENERIC_TIMEOUT, + &config_request_timeout, + cr); + } + return cr; +} + + +/** + * Begin asynchronous check for provider configurations. + * + * @param currencies the currencies to initiate the provider checks for + * @param[in,out] state to set provider list for + * @return #TALER_EC_NONE on success + */ +static enum TALER_ErrorCode +begin_provider_config_check (const json_t *currencies, + json_t *state) +{ + if (NULL == provider_list) + { + json_error_t error; + char *dn; + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + GNUNET_asprintf (&dn, + "%s/provider-list.json", + path); + GNUNET_free (path); + provider_list = json_load_file (dn, + JSON_COMPACT, + &error); + if (NULL == provider_list) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse `%s': %s at %d:%d (%d)\n", + dn, + error.text, + error.line, + error.column, + error.position); + GNUNET_free (dn); + return TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED; + } + GNUNET_free (dn); + } + + { + size_t index; + json_t *provider; + const json_t *provider_arr = json_object_get (provider_list, + "anastasis_provider"); + json_t *pl; + + pl = json_object (); + json_array_foreach (provider_arr, index, provider) + { + const char *url; + const char *cur; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("url", + &url), + GNUNET_JSON_spec_string ("currency", + &cur), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (provider, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (pl); + return TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED; + } + + { + bool found = false; + json_t *cu; + size_t off; + + json_array_foreach (currencies, off, cu) + { + const char *currency; + + currency = json_string_value (cu); + if (NULL == currency) + { + json_decref (pl); + return TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID; + } + found = (0 == strcasecmp (currency, + cur)); + } + if (! found) + continue; + } + GNUNET_assert (0 == + json_object_set_new (pl, + url, + json_object ())); + check_config (url); + } + GNUNET_assert (0 == + json_object_set_new (state, + "authentication_providers", + pl)); + } + return TALER_EC_NONE; +} + + +/** + * Function to validate an input by regular expression ("validation-regex"). + * + * @param input text to validate + * @param regexp regular expression to validate + * @return true if validation passed, else false + */ +static bool +validate_regex (const char *input, + const char *regexp) +{ + regex_t regex; + + if (0 != regcomp (®ex, + regexp, + REG_EXTENDED)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to compile regular expression `%s'.", + regexp); + return true; + } + /* check if input has correct form */ + if (0 != regexec (®ex, + input, + 0, + NULL, + 0)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Input `%s' does not match regex `%s'\n", + input, + regexp); + regfree (®ex); + return false; + } + regfree (®ex); + return true; +} + + +/** + * Function to load json containing country specific identity + * attributes. Uses a single-slot cache to avoid loading + * exactly the same attributes twice. + * + * @param country_code country code (e.g. "de") + * @return NULL on error + */ +static const json_t * +redux_id_attr_init (const char *country_code) +{ + static char redux_id_cc[3]; + char *dn; + json_error_t error; + + if (0 == strcmp (country_code, + redux_id_cc)) + return redux_id_attr; + + if (NULL != redux_id_attr) + { + json_decref (redux_id_attr); + redux_id_attr = NULL; + } + { + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return NULL; + } + GNUNET_asprintf (&dn, + "%s/redux.%s.json", + path, + country_code); + GNUNET_free (path); + } + redux_id_attr = json_load_file (dn, + JSON_COMPACT, + &error); + if (NULL == redux_id_attr) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse `%s': %s at %d:%d (%d)\n", + dn, + error.text, + error.line, + error.column, + error.position); + GNUNET_free (dn); + return NULL; + } + GNUNET_free (dn); + strncpy (redux_id_cc, + country_code, + sizeof (redux_id_cc)); + redux_id_cc[2] = '\0'; + return redux_id_attr; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "select_continent" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +select_continent (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *redux_countries = ANASTASIS_redux_countries_init_ (); + const json_t *root = json_object_get (redux_countries, + "countries"); + const json_t *continent; + json_t *countries; + + if (NULL == root) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED, + "'countries' missing"); + return NULL; + } + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + continent = json_object_get (arguments, + "continent"); + if (NULL == continent) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'continent' missing"); + return NULL; + } + countries = json_array (); + GNUNET_assert (NULL != countries); + { + size_t index; + const json_t *country; + bool found = false; + + json_array_foreach (root, index, country) + { + json_t *temp_continent = json_object_get (country, + "continent"); + if (1 == json_equal (continent, + temp_continent)) + { + GNUNET_assert (0 == + json_array_append (countries, + (json_t *) country)); + found = true; + } + } + if (! found) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'continent' unknown"); + return NULL; + } + } + redux_transition (state, + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING); + GNUNET_assert (0 == + json_object_set (state, + "selected_continent", + (json_t *) continent)); + GNUNET_assert (0 == + json_object_set_new (state, + "countries", + countries)); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "select_country" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return #ANASTASIS_ReduxAction + */ +static struct ANASTASIS_ReduxAction * +select_country (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *required_attrs; + const json_t *country_code; + const json_t *currencies; + const json_t *redux_id_attr; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + country_code = json_object_get (arguments, + "country_code"); + if (NULL == country_code) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'country_code' missing"); + return NULL; + } + + { + json_t *countries = json_object_get (state, + "countries"); + size_t index; + json_t *country; + bool found = false; + + json_array_foreach (countries, index, country) + { + json_t *cc = json_object_get (country, + "code"); + if (1 == json_equal (country_code, + cc)) + { + found = true; + break; + } + } + if (! found) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "specified country not on selected continent"); + return NULL; + } + } + + currencies = json_object_get (arguments, + "currencies"); + if ( (NULL == currencies) || + (! json_is_array (currencies)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'currencies' missing"); + return NULL; + } + /* We now have an idea of the currency, begin fetching + provider /configs (we likely need them later) */ + { + enum TALER_ErrorCode ec; + + ec = begin_provider_config_check (currencies, + state); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + ec, + NULL); + return NULL; + } + } + redux_id_attr = redux_id_attr_init (json_string_value (country_code)); + if (NULL == redux_id_attr) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_RESOURCE_MISSING, + json_string_value (country_code)); + return NULL; + } + required_attrs = json_object_get (redux_id_attr, + "required_attributes"); + if (NULL == required_attrs) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED, + "'required_attributes' missing"); + return NULL; + } + redux_transition (state, + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING); + GNUNET_assert (0 == + json_object_set (state, + "selected_country", + (json_t *) country_code)); + GNUNET_assert (0 == + json_object_set (state, + "currencies", + (json_t *) currencies)); + GNUNET_assert (0 == + json_object_set (state, + "required_attributes", + (json_t *) required_attrs)); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "unselect_continent" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +unselect_continent (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + redux_transition (state, + ANASTASIS_GENERIC_STATE_CONTINENT_SELECTING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_add_provider_to_state_ (const char *url, + json_t *state, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct ConfigRequest *cr; + struct ConfigReduxWaiting *w; + + cr = check_config (url); + w = GNUNET_new (struct ConfigReduxWaiting); + w->cr = cr; + w->state = json_incref (state); + w->cb = cb; + w->cb_cls = cb_cls; + w->ra.cleanup = &abort_provider_config_cb; + w->ra.cleanup_cls = w; + GNUNET_CONTAINER_DLL_insert (cr->w_head, + cr->w_tail, + w); + if (NULL == cr->co) + { + notify_waiting (cr); + return NULL; + } + return &w->ra; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "enter_user_attributes" action. + * Returns an #ANASTASIS_ReduxAction if operation is async. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +enter_user_attributes (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *attributes; + const json_t *required_attributes; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + attributes = json_object_get (arguments, + "identity_attributes"); + if (NULL == attributes) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'identity_attributes' missing"); + return NULL; + } + GNUNET_assert (0 == + json_object_set (state, + "identity_attributes", + (json_t *) attributes)); + + /* Verify required attributes are present and well-formed */ + required_attributes = json_object_get (state, + "required_attributes"); + if ( (NULL == required_attributes) || + (! json_is_array (required_attributes)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'required_attributes' must be an array"); + return NULL; + } + { + size_t index; + json_t *required_attribute; + + json_array_foreach (required_attributes, index, required_attribute) + { + const char *name; + const char *attribute_value; + const char *regexp = NULL; + const char *reglog = NULL; + int optional = false; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &name), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("validation-regex", + ®exp)), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("validation-logic", + ®log)), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_boolean ("optional", + &optional)), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (required_attribute, + spec, + NULL, NULL)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'required_attributes' lacks required fields"); + return NULL; + } + attribute_value = json_string_value (json_object_get (attributes, + name)); + if (NULL == attribute_value) + { + if (optional) + continue; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Request is missing required attribute `%s'\n", + name); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_GENERIC_PARAMETER_MISSING, + name); + return NULL; + } + if ( (NULL != regexp) && + (! validate_regex (attribute_value, + regexp)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_REGEX_FAILED, + name); + return NULL; + } + + if (NULL != reglog) + { + bool (*regfun)(const char *); + + regfun = dlsym (RTLD_DEFAULT, + reglog); + if (NULL == regfun) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Custom validation function `%s' is not available: %s\n", + reglog, + dlerror ()); + } + else if (! regfun (attribute_value)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED, + name); + return NULL; + } + } + } /* end for all attributes loop */ + } /* end for all attributes scope */ + + /* Transition based on mode */ + { + const char *s_mode = get_state_mode (state); + + if (0 == strcmp (s_mode, + "backup_state")) + { + GNUNET_assert (0 == + json_object_set_new ( + state, + "backup_state", + json_string ( + ANASTASIS_backup_state_to_string_ ( + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING)))); + return ANASTASIS_REDUX_backup_begin_ (state, + arguments, + cb, + cb_cls); + } + else + { + GNUNET_assert (0 == + json_object_set_new ( + state, + "recovery_state", + json_string ( + ANASTASIS_recovery_state_to_string_ ( + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING)))); + return ANASTASIS_REDUX_recovery_challenge_begin_ (state, + arguments, + cb, + cb_cls); + } + } +} + + +/** + * DispatchHandler/Callback function which is called for a + * "add_provider" action. Adds another Anastasis provider + * to the list of available providers for storing information. + * + * @param state state to operate on + * @param arguments arguments with a provider URL to add + * @param cb callback to call during/after operation + * @param cb_cls callback closure + */ +static struct ANASTASIS_ReduxAction * +add_provider (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + if (ANASTASIS_add_provider_ (state, + arguments, + cb, + cb_cls)) + return NULL; + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +bool +ANASTASIS_add_provider_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *urls; + json_t *tlist; + + tlist = json_object_get (state, + "authentication_providers"); + if (NULL == tlist) + { + tlist = json_object (); + GNUNET_assert (NULL != tlist); + GNUNET_assert (0 == + json_object_set_new (state, + "authentication_providers", + tlist)); + } + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return true; + } + urls = json_object_get (arguments, + "urls"); + if (NULL == urls) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'urls' missing"); + return true; + } + { + size_t index; + json_t *url; + + json_array_foreach (urls, index, url) + { + const char *url_str = json_string_value (url); + + if (NULL == url_str) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'urls' must be strings"); + return true; + } + GNUNET_assert (0 == + json_object_set_new (tlist, + url_str, + json_object ())); + } + } + return false; +} + + +struct ANASTASIS_ReduxAction * +ANASTASIS_back_generic_decrement_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const char *s_mode = get_state_mode (state); + const char *state_string = json_string_value (json_object_get (state, + s_mode)); + + (void) arguments; + GNUNET_assert (NULL != state_string); + if (0 == strcmp ("backup_state", + s_mode)) + { + enum ANASTASIS_BackupState state_index; + + state_index = ANASTASIS_backup_state_from_string_ (state_string); + GNUNET_assert (state_index > 0); + state_index = state_index - 1; + + GNUNET_assert (0 == + json_object_set_new ( + state, + s_mode, + json_string ( + ANASTASIS_backup_state_to_string_ (state_index)))); + } + else + { + enum ANASTASIS_RecoveryState state_index; + + state_index = ANASTASIS_recovery_state_from_string_ (state_string); + GNUNET_assert (state_index > 0); + state_index = state_index - 1; + GNUNET_assert (0 == + json_object_set_new ( + state, + s_mode, + json_string ( + ANASTASIS_recovery_state_to_string_ (state_index)))); + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * Callback function which is called by the reducer in dependence of + * given state and action. + * + * @param state the previous state to operate on + * @param arguments the arguments needed by operation to operate on state + * @param cb Callback function which returns the new state + * @param cb_cls closure for @a cb + * @return handle to cancel async actions, NULL if @a cb was already called + */ +typedef struct ANASTASIS_ReduxAction * +(*DispatchHandler)(json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +struct ANASTASIS_ReduxAction * +ANASTASIS_redux_action (const json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct Dispatcher + { + enum ANASTASIS_GenericState redux_state; + const char *redux_action; + DispatchHandler fun; + } dispatchers[] = { + { + ANASTASIS_GENERIC_STATE_CONTINENT_SELECTING, + "select_continent", + &select_continent + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "unselect_continent", + &unselect_continent + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "select_country", + &select_country + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "select_continent", + &select_continent + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "unselect_continent", + &unselect_continent + }, + { + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING, + "enter_user_attributes", + &enter_user_attributes + }, + { + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING, + "add_provider", + &add_provider + }, + { + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { ANASTASIS_GENERIC_STATE_ERROR, NULL, NULL } + }; + bool recovery_mode = false; + const char *s = json_string_value (json_object_get (state, + "backup_state")); + enum ANASTASIS_GenericState gs; + + if (NULL == s) + { + s = json_string_value (json_object_get (state, + "recovery_state")); + if (NULL == s) + { + GNUNET_break_op (0); + cb (cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + NULL); + return NULL; + } + recovery_mode = true; + } + gs = ANASTASIS_generic_state_from_string_ (s); + { + json_t *new_state; + struct ANASTASIS_ReduxAction *ret; + + new_state = json_deep_copy (state); + GNUNET_assert (NULL != new_state); + if (gs != ANASTASIS_GENERIC_STATE_ERROR) + { + for (unsigned int i = 0; NULL != dispatchers[i].fun; i++) + { + if ( (gs == dispatchers[i].redux_state) && + (0 == strcmp (action, + dispatchers[i].redux_action)) ) + { + ret = dispatchers[i].fun (new_state, + arguments, + cb, + cb_cls); + json_decref (new_state); + return ret; + } + } + } + if (recovery_mode) + { + ret = ANASTASIS_recovery_action_ (new_state, + action, + arguments, + cb, + cb_cls); + } + else + { + ret = ANASTASIS_backup_action_ (new_state, + action, + arguments, + cb, + cb_cls); + } + json_decref (new_state); + return ret; + } +} + + +void +ANASTASIS_redux_action_cancel (struct ANASTASIS_ReduxAction *ra) +{ + ra->cleanup (ra->cleanup_cls); +} + + +json_t * +ANASTASIS_REDUX_load_continents_ () +{ + const json_t *countries; + json_t *continents; + const json_t *redux_countries = ANASTASIS_redux_countries_init_ (); + + if (NULL == redux_countries) + { + GNUNET_break (0); + return NULL; + } + countries = json_object_get (redux_countries, + "countries"); + if (NULL == countries) + { + GNUNET_break (0); + return NULL; + } + continents = json_array (); + GNUNET_assert (NULL != continents); + + { + json_t *country; + size_t index; + + json_array_foreach (countries, index, country) + { + json_t *ex = NULL; + const json_t *continent; + + continent = json_object_get (country, + "continent"); + if ( (NULL == continent) || + (! json_is_string (continent)) ) + { + GNUNET_break (0); + continue; + } + { + size_t inner_index; + json_t *inner_continent; + + json_array_foreach (continents, inner_index, inner_continent) + { + const json_t *name; + + name = json_object_get (inner_continent, + "name"); + if (1 == json_equal (continent, + name)) + { + ex = inner_continent; + break; + } + } + } + if (NULL == ex) + { + ex = json_pack ("{s:O}", + "name", + continent); + GNUNET_assert (0 == + json_array_append_new (continents, + ex)); + } + + { + json_t *i18n_continent; + json_t *name_ex; + + i18n_continent = json_object_get (country, + "continent_i18n"); + name_ex = json_object_get (ex, + "name_i18n"); + if (NULL != i18n_continent) + { + const char *lang; + json_t *trans; + + json_object_foreach (i18n_continent, lang, trans) + { + if (NULL == name_ex) + { + name_ex = json_object (); + json_object_set_new (ex, + "name_i18n", + name_ex); + } + if (NULL == json_object_get (name_ex, + lang)) + { + json_object_set (name_ex, + lang, + trans); + } + } + } + } + } + } + return json_pack ("{s:o}", + "continents", + continents); +} diff --git a/src/reducer/anastasis_api_redux.h b/src/reducer/anastasis_api_redux.h new file mode 100644 index 0000000..19d9466 --- /dev/null +++ b/src/reducer/anastasis_api_redux.h @@ -0,0 +1,347 @@ +/* + 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 +*/ +/** + * @file redux/anastasis_api_redux.h + * @brief anastasis reducer api, internal data structures + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#ifndef ANASTASIS_API_REDUX_H +#define ANASTASIS_API_REDUX_H + + +#define ANASTASIS_GENERIC_STATES(REDUX_STATE) \ + REDUX_STATE (ERROR) \ + REDUX_STATE (CONTINENT_SELECTING) \ + REDUX_STATE (COUNTRY_SELECTING) \ + REDUX_STATE (USER_ATTRIBUTES_COLLECTING) + +#define GENERATE_GENERIC_ENUM(ENUM) ANASTASIS_GENERIC_STATE_ ## ENUM, + +enum ANASTASIS_GenericState +{ + ANASTASIS_GENERIC_STATES (GENERATE_GENERIC_ENUM) +}; + +#undef GENERATE_GENERIC_ENUM + +#define ANASTASIS_BACKUP_STATES(REDUX_STATE) \ + ANASTASIS_GENERIC_STATES (REDUX_STATE) \ + REDUX_STATE (AUTHENTICATIONS_EDITING) \ + REDUX_STATE (POLICIES_REVIEWING) \ + REDUX_STATE (SECRET_EDITING) \ + REDUX_STATE (TRUTHS_PAYING) \ + REDUX_STATE (POLICIES_PAYING) \ + REDUX_STATE (BACKUP_FINISHED) + +#define GENERATE_BACKUP_ENUM(ENUM) ANASTASIS_BACKUP_STATE_ ## ENUM, + +enum ANASTASIS_BackupState +{ + ANASTASIS_BACKUP_STATES (GENERATE_BACKUP_ENUM) +}; + +#undef GENERATE_BACKUP_ENUM + +#define ANASTASIS_RECOVERY_STATES(REDUX_STATE) \ + ANASTASIS_GENERIC_STATES (REDUX_STATE) \ + REDUX_STATE (SECRET_SELECTING) \ + REDUX_STATE (CHALLENGE_SELECTING) \ + REDUX_STATE (CHALLENGE_PAYING) \ + REDUX_STATE (CHALLENGE_SOLVING) \ + REDUX_STATE (RECOVERY_FINISHED) + +#define GENERATE_RECOVERY_ENUM(ENUM) ANASTASIS_RECOVERY_STATE_ ## ENUM, + +enum ANASTASIS_RecoveryState +{ + ANASTASIS_RECOVERY_STATES (GENERATE_RECOVERY_ENUM) +}; + +#undef GENERATE_RECOVERY_ENUM + + +/** + * CURL context to be used by all operations. + */ +extern struct GNUNET_CURL_Context *ANASTASIS_REDUX_ctx_; + + +/** + * 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); + + +/** + * Produce an initial state with an initialized list of + * continents. + */ +json_t * +ANASTASIS_REDUX_load_continents_ (void); + + +/** + * Returns the enum value to a string value of a state. + * + * @param state_string + * @return #ANASTASIS_GENERIC_STATE_ERROR on error + */ +enum ANASTASIS_GenericState +ANASTASIS_generic_state_from_string_ (const char *state_string); + + +/** + * Returns the string value of a state. + * + * @param state_string + * @return NULL on error + */ +const char * +ANASTASIS_generic_state_to_string_ (enum ANASTASIS_GenericState gs); + + +/** + * Returns the enum value to a string value of a state. + * + * @param state_string + * @return #ANASTASIS_BACKUP_STATE_ERROR on error + */ +enum ANASTASIS_BackupState +ANASTASIS_backup_state_from_string_ (const char *state_string); + + +/** + * Returns the string value of a state. + * + * @param state_string + * @return NULL on error + */ +const char * +ANASTASIS_backup_state_to_string_ (enum ANASTASIS_BackupState bs); + + +/** + * Returns the enum value to a string value of a state. + * + * @param state_string + * @return XXX on error + */ +enum ANASTASIS_RecoveryState +ANASTASIS_recovery_state_from_string_ (const char *state_string); + + +/** + * Returns the string value of a state. + * + * @param state_string + * @return NULL on error + */ +const char * +ANASTASIS_recovery_state_to_string_ (enum ANASTASIS_RecoveryState rs); + + +/** + * Function to return a json error response. + * + * @param cb callback to give error to + * @param cb_cls callback closure + * @param ec error code + * @param detail error detail + */ +void +ANASTASIS_redux_fail_ (ANASTASIS_ActionCallback cb, + void *cb_cls, + enum TALER_ErrorCode ec, + const char *detail); + + +/** + * DispatchHandler/Callback function which is called for a + * "add_provider" action. Adds another Anastasis provider + * to the list of available providers for storing information. + * + * @param state state to operate on + * @param arguments arguments with a provider URL to add + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return true if @a cb was invoked + */ +bool +ANASTASIS_add_provider_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * Adds the server configuration of the Anastasis provider + * at @a url to the json @a state. Checks if we have + * the provider information already available. If so, + * imports it into @a state. If not, queries the provider, + * generating a success or failure outcome asynchronously. + * + * @param cr the config request + * @param[in,out] state the json state to operate on + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return handle to cancel asynchronous operation, NULL if + * we completed synchronously + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_add_provider_to_state_ (const char *url, + json_t *state, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * A generic DispatchHandler/Callback function which is called for a + * "back" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL (no asynchronous action) + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_back_generic_decrement_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * Function to load json containing all countries. + * Returns the countries. + * + * @return json_t * + */ +const json_t * +ANASTASIS_redux_countries_init_ (void); + + +/** + * 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 the CURL context used to connect to the backend + * @param[in,out] state input/output state (to be modified) + * @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 + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_recovery_action_ (json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * DispatchHandler/Callback function which is called for a + * "enter_user_attributes" action after verifying that the + * arguments provided were OK and the state transition was + * initiated. Begins the actual recovery logic. + * + * Returns an #ANASTASIS_ReduxAction. + * + * @param state state to operate on + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_recovery_challenge_begin_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * DispatchHandler/Callback function which is called for a + * "enter_user_attributes" action after verifying that the + * arguments provided were OK and the state transition was + * initiated. Begins the actual backup logic. + * + * Returns an #ANASTASIS_ReduxAction. + * + * @param state state to operate on + * @param cb callback to call during/after operation + * @param cb_cls callback closure + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_backup_begin_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * 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 the CURL context used to connect to the backend + * @param[in,out] state input/output state (to be modified) + * @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 + */ +struct ANASTASIS_ReduxAction * +ANASTASIS_backup_action_ (json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +/** + * Generic container for an action with asynchronous activities. + */ +struct ANASTASIS_ReduxAction +{ + /** + * Function to call to clean up. + */ + void (*cleanup)(void *cls); + + /** + * Action-specific state, closure for @e cleanup. + */ + void *cleanup_cls; +}; + + +#endif diff --git a/src/reducer/validation_CH_AHV.c b/src/reducer/validation_CH_AHV.c new file mode 100644 index 0000000..e6a2f25 --- /dev/null +++ b/src/reducer/validation_CH_AHV.c @@ -0,0 +1,57 @@ +/* + 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 +*/ +/** + * @file redux/validation_CH_AHV.c + * @brief anastasis reducer api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include +#include + +/** + * Function to validate a Swiss AHV number. + * + * @param avh_number ahv number to validate (input) + * @return true if validation passed, else false + */ +bool +CH_AHV_check (const char *ahv_number) +{ + unsigned int checknum; + unsigned int next_ten; + const char *pos = &ahv_number[strlen (ahv_number) - 1]; + bool phase = true; + unsigned int calculation = 0; + + checknum = *pos - 48; + while (pos > ahv_number) + { + pos--; + if ('.' == *pos) + continue; + if (phase) + calculation += ((*pos - 48) * 3); + else + calculation += *pos - 48; + phase = ! phase; + } + /* round up to the next ten */ + next_ten = ((calculation + 9) / 10) * 10; + calculation = next_ten - calculation; + return (checknum == calculation); +} diff --git a/src/reducer/validation_CZ_BN.c b/src/reducer/validation_CZ_BN.c new file mode 100644 index 0000000..4598c42 --- /dev/null +++ b/src/reducer/validation_CZ_BN.c @@ -0,0 +1,59 @@ +/* + 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 +*/ +/** + * @file redux/validation_CZ_BN.c + * @brief validation of Czeck Birth Numbers + * @author Christian Grothoff + */ +#include +#include +#include +#include + +/** + * Function to validate a Check birth number. Basically, + * if it has 10 digits, it must be divisible by 11. + * + * @param b_number birth number to validate (input) + * @return true if b_number is valid + */ +bool +CZ_BN_check (const char *b_number) +{ + unsigned long long n; + char dummy; + char in[11]; + + if (10 == strlen (b_number)) + return true; + if (11 != strlen (b_number)) + return false; + if (b_number[6] != '/') + return false; + memcpy (in, + b_number, + 6); + memcpy (&in[6], + &b_number[7], + 4); + in[10] = '\0'; + if (1 != sscanf (in, + "%llu%c", + &n, + &dummy)) + return false; + return 0 == (n % 11); +} diff --git a/src/reducer/validation_DE_SVN.c b/src/reducer/validation_DE_SVN.c new file mode 100644 index 0000000..bfc406d --- /dev/null +++ b/src/reducer/validation_DE_SVN.c @@ -0,0 +1,98 @@ +/* + 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 +*/ +/** + * @file redux/validation_DE_SVN.c + * @brief + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include +#include + +/** + * Sum up all digits in @a v and return the result. + */ +static unsigned int +q (unsigned int v) +{ + unsigned int r = 0; + + while (0 != v) + { + r += v % 10; + v = v / 10; + } + return r; +} + + +/** + * Function to validate a German Social Security number. + * + * See https://www.financescout24.de/wissen/ratgeber/sozialversicherungsnummer + * and https://de.wikipedia.org/wiki/Versicherungsnummer + * for the structure! + * + * @param avh_number ahv number to validate (input) + * @return true if validation passed, else false + */ +bool +DE_SVN_check (const char *ssn_number) +{ + static const unsigned int factors[] = { + 2, 1, 2, 5, 7, 1, 2, 1, 2, 1, 2, 1 + }; + unsigned int sum = 0; + + if (strlen (ssn_number) != 12) + return false; + for (unsigned int i = 0; i<8; i++) + { + unsigned char c = (unsigned char) ssn_number[i]; + + if ( ('0' > c) || ('9' < c) ) + return false; + sum += q ((c - '0') * factors[i]); + } + { + unsigned char c = (unsigned char) ssn_number[8]; + unsigned int v = (c - 'A' + 1); + + if ( ('A' > c) || ('Z' < c) ) + return false; + sum += q ((v / 10) * factors[8]); + sum += q ((v % 10) * factors[9]); + } + for (unsigned int i = 9; i<11; i++) + { + unsigned char c = ssn_number[i]; + + if ( ('0' > c) || ('9' < c) ) + return false; + sum += q ((c - '0') * factors[i + 1]); + } + if (ssn_number[11] != '0' + (sum % 10)) + return false; + { + unsigned int month = (ssn_number[4] - '0') * 10 + (ssn_number[5] - '0'); + + if ( (0 == month) || + (12 < month) ) + return false; + } + return true; +} diff --git a/src/reducer/validation_DE_TIN.c b/src/reducer/validation_DE_TIN.c new file mode 100644 index 0000000..9c97365 --- /dev/null +++ b/src/reducer/validation_DE_TIN.c @@ -0,0 +1,57 @@ +/* + 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 +*/ +/** + * @file redux/validation_DE_TIN.c + * @brief validation logic for German taxpayer identification numbers + * @author Christian Grothoff + */ +#include +#include + + +/** + * Function to validate a German Taxpayer identification number. + * + * See https://de.wikipedia.org/wiki/Steuerliche_Identifikationsnummer + * for the structure! + * + * @param tin_number tax number to validate (input) + * @return true if validation passed, else false + */ +bool +DE_TIN_check (const char *tin_number) +{ + unsigned int csum; + unsigned int product = 10; + + if (strlen (tin_number) != 11) + return false; + for (unsigned int i = 0; i<10; i++) + { + unsigned int sum = ((tin_number[i] - '0') + product) % 10; + if (0 == sum) + sum = 10; + product = sum * 2 % 11; + } + csum = 11 - product; + if (10 == csum) + csum = 0; + if (tin_number[10] != '0' + csum) + return false; + if (tin_number[0] == '0') + return false; + return true; +} diff --git a/src/reducer/validation_IN_AADHAR.c b/src/reducer/validation_IN_AADHAR.c new file mode 100644 index 0000000..939ee72 --- /dev/null +++ b/src/reducer/validation_IN_AADHAR.c @@ -0,0 +1,113 @@ +/* + 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 +*/ +/** + * @file redux/validation_IN_AADHAR.c + * @brief validation logic for Indian Aadhar numbers + * @author Christian Grothoff + */ +#include +#include +#include + +/** + * The multiplication table. + */ +static int m[10][10] = { + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 2, 3, 4, 0, 6, 7, 8, 9, 5}, + {2, 3, 4, 0, 1, 7, 8, 9, 5, 6}, + {3, 4, 0, 1, 2, 8, 9, 5, 6, 7}, + {4, 0, 1, 2, 3, 9, 5, 6, 7, 8}, + {5, 9, 8, 7, 6, 0, 4, 3, 2, 1}, + {6, 5, 9, 8, 7, 1, 0, 4, 3, 2}, + {7, 6, 5, 9, 8, 2, 1, 0, 4, 3}, + {8, 7, 6, 5, 9, 3, 2, 1, 0, 4}, + {9, 8, 7, 6, 5, 4, 3, 2, 1, 0} +}; + + +/** + * The permutation table. + */ +static int p[10][10] = { + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + {1, 5, 7, 6, 2, 8, 3, 0, 9, 4}, + {5, 8, 0, 3, 7, 9, 6, 1, 4, 2}, + {8, 9, 1, 6, 0, 4, 3, 5, 2, 7}, + {9, 4, 5, 3, 1, 2, 6, 8, 7, 0}, + {4, 2, 8, 6, 5, 7, 3, 9, 0, 1}, + {2, 7, 9, 3, 8, 0, 6, 4, 1, 5}, + {7, 0, 4, 6, 9, 1, 3, 2, 5, 8} +}; + + +/** + * Converts a string to a reversed integer array. + * + * @param input The numeric string data converted to reversed int array. + * @param[out] Integer array containing the digits in the numeric string + * in reverse order + */ +static bool +string_to_vals (const char *input, + int output[12]) +{ + unsigned int off = 0; + + for (unsigned int i = 0; i < 12;) + { + int c = input[i + off]; + + if (0 == c) + return false; + if (isspace (c)) + { + off++; + continue; + } + if (! isdigit (c)) + return false; + output[11 - i++] = c - '0'; + } + if ('\0' != input[12 + off]) + return false; + return true; +} + + +/** + * Function to validate an Indian Aadhar number. + * + * See https://www.geeksforgeeks.org/how-to-check-aadhar-number-is-valid-or-not-using-regular-expression/ + * and http://en.wikipedia.org/wiki/Verhoeff_algorithm/. + * + * @param aadhar_number aadhar number to validate (input) + * @return true if validation passed, else false + */ +bool +IN_AADHAR_check (const char *aadhar_number) +{ + int c = 0; + int vals[12]; + + if (! string_to_vals (aadhar_number, + vals)) + return false; + for (unsigned int i = 0; i < 12; i++) + c = m[c][p[(i % 8)][vals[i]]]; + + return (0 == c); +} diff --git a/src/reducer/validation_IT_CF.c b/src/reducer/validation_IT_CF.c new file mode 100644 index 0000000..ecdad90 --- /dev/null +++ b/src/reducer/validation_IT_CF.c @@ -0,0 +1,198 @@ +/* + This file is part of Anastasis + Copyright (C) 2021 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file redux/validation_IT_CF.c + * @brief validation of Italian Code Fiscales + * @author Christian Grothoff + */ +#include +#include +#include +#include + +struct MapEntry +{ + char in; + unsigned int out; +}; + + +static const struct MapEntry odd[] = { + { '0', 1}, + { '9', 21}, + { 'I', 19 }, + { 'R', 8}, + { '1', 0}, + { 'A', 1}, + { 'J', 21}, + { 'S', 12}, + {'2', 5}, + {'B', 0}, + {'K', 2}, + {'T', 14}, + {'3', 7}, + {'C', 5}, + {'L', 4}, + {'U', 16}, + {'4', 9}, + {'D', 7}, + {'M', 18}, + {'V', 10}, + {'5', 13}, + {'E', 9}, + {'N', 20}, + {'W', 22}, + {'6', 15}, + {'F', 13}, + {'O', 11}, + {'X', 25}, + {'7', 17}, + {'G', 15}, + {'P', 3}, + {'Y', 24}, + {'8', 19}, + {'H', 17}, + {'Q', 6}, + {'Z', 23}, + {'\0', 0} +}; + + +static const struct MapEntry even[] = { + { '0', 0}, + { '1', 1}, + { '2', 2 }, + { '3', 3}, + { '4', 4}, + { '5', 5}, + { '6', 6 }, + { '7', 7 }, + {'8', 8}, + {'9', 9}, + {'A', 0}, + {'B', 1}, + {'C', 2}, + {'D', 3}, + {'E', 4}, + {'F', 5}, + {'G', 6}, + {'H', 7}, + {'I', 8}, + {'J', 9}, + {'K', 10}, + {'L', 11}, + {'M', 12}, + {'N', 13}, + {'O', 14}, + {'P', 15}, + {'Q', 16}, + {'R', 17}, + {'S', 18}, + {'T', 19}, + {'U', 20}, + {'V', 21}, + {'W', 22}, + {'X', 23}, + {'Y', 24}, + {'Z', 25}, + {'\0', 0} +}; + + +static const struct MapEntry rem[] = { + {'A', 0}, + {'B', 1}, + {'C', 2}, + {'D', 3}, + {'E', 4}, + {'F', 5}, + {'G', 6}, + {'H', 7}, + {'I', 8}, + {'J', 9}, + {'K', 10}, + {'L', 11}, + {'M', 12}, + {'N', 13}, + {'O', 14}, + {'P', 15}, + {'Q', 16}, + {'R', 17}, + {'S', 18}, + {'T', 19}, + {'U', 20}, + {'V', 21}, + {'W', 22}, + {'X', 23}, + {'Y', 24}, + {'Z', 25}, + {'\0', 0} +}; + + +/** + * Lookup @a in in @a map. Set @a fail to true if @a in is not found. + * + * @param map character map to search + * @param in character to search for + * @param[out] fail set to true on error + * @return map value, 0 on error + */ +static unsigned int +lookup (const struct MapEntry *map, + char in, + bool *fail) +{ + for (unsigned int i = 0; '\0' != map[i].in; i++) + if (in == map[i].in) + return map[i].out; + *fail = true; + return 0; +} + + +/** + * Function to validate an italian code fiscale number. + * See https://en.wikipedia.org/wiki/Italian_fiscal_code + * + * @param cf_number square number to validate (input) + * @return true if @a cf_number is a valid, else false + */ +bool +IT_CF_check (const char *cf_number) +{ + unsigned int sum = 0; + bool fail = false; + + if (strlen (cf_number) != 16) + return false; + for (unsigned int i = 0; i<15; i += 2) + sum += lookup (odd, + cf_number[i], + &fail); + for (unsigned int i = 1; i<15; i += 2) + sum += lookup (even, + cf_number[i], + &fail); + sum %= 26; + if (sum != lookup (rem, + cf_number[15], + &fail)) + return false; + if (fail) + return false; + return true; +} diff --git a/src/reducer/validation_XX_SQUARE.c b/src/reducer/validation_XX_SQUARE.c new file mode 100644 index 0000000..fa3ebfb --- /dev/null +++ b/src/reducer/validation_XX_SQUARE.c @@ -0,0 +1,48 @@ +/* + 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 +*/ +/** + * @file redux/validation_XX_PRIME.c + * @brief anastasis reducer api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include +#include +#include +#include + +/** + * Function to validate a square number. + * + * @param sq_number square number to validate (input) + * @return true if sq_number is a square, else false + */ +bool +XX_SQUARE_check (const char *sq_number) +{ + unsigned long long n; + unsigned long long r; + char dummy; + + if (1 != sscanf (sq_number, + "%llu%c", + &n, + &dummy)) + return false; + r = sqrt (n); + return (n == r * r); +} diff --git a/src/reducer/validation_XY_PRIME.c b/src/reducer/validation_XY_PRIME.c new file mode 100644 index 0000000..ab24014 --- /dev/null +++ b/src/reducer/validation_XY_PRIME.c @@ -0,0 +1,53 @@ +/* + 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 +*/ +/** + * @file redux/validation_XY_PRIME.c + * @brief anastasis reducer api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include +#include +#include +#include + +/** + * Function to validate a prime number. + * + * @param pr_number prime number to validate (input) + * @return true if pr_number is a prime, else false + */ +bool +XY_PRIME_check (const char *pr_number) +{ + unsigned long long n; + char dummy; + gcry_mpi_t p; + bool is_prime; + + if (1 != sscanf (pr_number, + "%llu%c", + &n, + &dummy)) + return false; + p = gcry_mpi_set_ui (NULL, + (unsigned long) n); + is_prime = (0 == gcry_prime_check (p, + 0)); + gcry_mpi_release (p); + return is_prime; +} diff --git a/src/restclient/Makefile.am b/src/restclient/Makefile.am new file mode 100644 index 0000000..075d3a7 --- /dev/null +++ b/src/restclient/Makefile.am @@ -0,0 +1,42 @@ +# 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 + +.NOTPARALLEL: + +lib_LTLIBRARIES = \ + libanastasisrest.la + +libanastasisrest_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined +libanastasisrest_la_SOURCES = \ + anastasis_api_config.c \ + anastasis_api_policy_store.c \ + anastasis_api_truth_store.c \ + anastasis_api_policy_lookup.c \ + anastasis_api_keyshare_lookup.c \ + anastasis_api_curl_defaults.c anastasis_api_curl_defaults.h +libanastasisrest_la_LIBADD = \ + -lgnunetcurl \ + -lgnunetjson \ + -lgnunetutil \ + -ljansson \ + -ltalerjson \ + -ltalerutil \ + -ltalermerchant \ + -ltalerjson \ + $(XLIB) + +if HAVE_LIBCURL +libanastasisrest_la_LIBADD += -lcurl +else +if HAVE_LIBGNURL +libanastasisrest_la_LIBADD += -lgnurl +endif +endif + diff --git a/src/restclient/anastasis_api_config.c b/src/restclient/anastasis_api_config.c new file mode 100644 index 0000000..cf0846b --- /dev/null +++ b/src/restclient/anastasis_api_config.c @@ -0,0 +1,317 @@ +/* + 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 +*/ +/** + * @file lib/anastasis_api_config.c + * @brief Implementation of the /config GET + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include +#include /* just for HTTP status codes */ +#include "anastasis_service.h" +#include "anastasis_api_curl_defaults.h" +#include +#include + + +/** + * Which version of the Taler protocol is implemented + * by this library? Used to determine compatibility. + */ +#define ANASTASIS_PROTOCOL_CURRENT 0 + +/** + * How many versions are we backwards compatible with? + */ +#define ANASTASIS_PROTOCOL_AGE 0 + + +struct ANASTASIS_ConfigOperation +{ + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * The callback to pass the backend response to + */ + ANASTASIS_ConfigCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + +}; + + +/** + * Function called when we're done processing the + * HTTP /config request. + * + * @param cls the `struct ANASTASIS_ConfigOperation` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_config_finished (void *cls, + long response_code, + const void *response) +{ + struct ANASTASIS_ConfigOperation *co = cls; + const json_t *json = response; + + co->job = NULL; + switch (response_code) + { + case 0: + /* Hard error */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Backend `%s' failed to respond to GET /config\n", + co->url); + break; + case MHD_HTTP_OK: + { + const char *name; + struct ANASTASIS_Config acfg; + json_t *methods; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &name), + GNUNET_JSON_spec_string ("business_name", + &acfg.business_name), + GNUNET_JSON_spec_string ("version", + &acfg.version), + GNUNET_JSON_spec_string ("currency", + &acfg.currency), + GNUNET_JSON_spec_json ("methods", + &methods), + GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", + &acfg.storage_limit_in_megabytes), + TALER_JSON_spec_amount_any ("annual_fee", + &acfg.annual_fee), + TALER_JSON_spec_amount_any ("truth_upload_fee", + &acfg.truth_upload_fee), + TALER_JSON_spec_amount_any ("liability_limit", + &acfg.liability_limit), + GNUNET_JSON_spec_fixed_auto ("server_salt", + &acfg.salt), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + response_code = 0; + break; + } + if (0 != strcmp (name, + "anastasis")) + { + GNUNET_JSON_parse_free (spec); + response_code = 0; + break; + } + { + unsigned int age; + unsigned int revision; + unsigned int current; + char dummy; + + if (3 != sscanf (acfg.version, + "%u:%u:%u%c", + ¤t, + &revision, + &age, + &dummy)) + { + GNUNET_break_op (0); + response_code = 0; + GNUNET_JSON_parse_free (spec); + break; + } + if ( (ANASTASIS_PROTOCOL_CURRENT < current) && + (ANASTASIS_PROTOCOL_CURRENT < current - age) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Provider protocol version too new\n"); + response_code = 0; + GNUNET_JSON_parse_free (spec); + break; + } + if ( (ANASTASIS_PROTOCOL_CURRENT > current) && + (ANASTASIS_PROTOCOL_CURRENT - ANASTASIS_PROTOCOL_AGE > current) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Provider protocol version too old\n"); + GNUNET_break_op (0); + response_code = 0; + GNUNET_JSON_parse_free (spec); + break; + } + } + if ( (GNUNET_OK != + TALER_amount_cmp_currency (&acfg.liability_limit, + &acfg.annual_fee)) || + (0 != + strcasecmp (acfg.currency, + acfg.annual_fee.currency)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + response_code = 0; + break; + } + + if (! json_is_array (methods)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + response_code = 0; + break; + } + acfg.methods_length = json_array_size (methods); + { + struct ANASTASIS_AuthorizationMethodConfig mcfg[GNUNET_NZL ( + acfg.methods_length)]; + + for (unsigned int i = 0; itype), + TALER_JSON_spec_amount_any ("cost", + &m->usage_fee), + GNUNET_JSON_spec_end () + }; + + if ( (GNUNET_OK != + GNUNET_JSON_parse (json_array_get (methods, + i), + spec, + NULL, NULL)) || + (0 != + strcasecmp (m->usage_fee.currency, + acfg.currency)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + response_code = 0; + goto end; + } + } + acfg.methods = mcfg; + co->cb (co->cb_cls, + MHD_HTTP_OK, + &acfg); + GNUNET_JSON_parse_free (spec); + ANASTASIS_config_cancel (co); + return; + } + } + case MHD_HTTP_BAD_REQUEST: + /* This should never happen, either us or the anastasis server is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify */ + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u\n", + (unsigned int) response_code); + GNUNET_break_op (0); + break; + } +end: + co->cb (co->cb_cls, + response_code, + NULL); + ANASTASIS_config_cancel (co); +} + + +struct ANASTASIS_ConfigOperation * +ANASTASIS_get_config (struct GNUNET_CURL_Context *ctx, + const char *base_url, + ANASTASIS_ConfigCallback cb, + void *cb_cls) +{ + struct ANASTASIS_ConfigOperation *co; + + co = GNUNET_new (struct ANASTASIS_ConfigOperation); + co->url = TALER_url_join (base_url, + "config", + NULL); + co->ctx = ctx; + co->cb = cb; + co->cb_cls = cb_cls; + { + CURL *eh; + + eh = ANASTASIS_curl_easy_get_ (co->url); + co->job = GNUNET_CURL_job_add2 (ctx, + eh, + GNUNET_NO, + &handle_config_finished, + co); + } + if (NULL == co->job) + { + GNUNET_free (co->url); + GNUNET_free (co); + return NULL; + } + return co; +} + + +void +ANASTASIS_config_cancel (struct ANASTASIS_ConfigOperation *co) +{ + if (NULL != co->job) + { + GNUNET_CURL_job_cancel (co->job); + co->job = NULL; + } + GNUNET_free (co->url); + GNUNET_free (co); +} + + +/* end of anastasis_api_config.c */ diff --git a/src/restclient/anastasis_api_curl_defaults.c b/src/restclient/anastasis_api_curl_defaults.c new file mode 100644 index 0000000..27b965f --- /dev/null +++ b/src/restclient/anastasis_api_curl_defaults.c @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 + +*/ +/** + * @file lib/anastasis_api_curl_defaults.c + * @brief curl easy handle defaults + * @author Florian Dold + */ +#include "platform.h" +#include "anastasis_api_curl_defaults.h" + +CURL * +ANASTASIS_curl_easy_get_ (const char *url) +{ + CURL *eh; + + eh = curl_easy_init (); + if (NULL == eh) + return NULL; + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + url)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_FOLLOWLOCATION, + 1L)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_TCP_FASTOPEN, + 1L)); + return eh; +} diff --git a/src/restclient/anastasis_api_curl_defaults.h b/src/restclient/anastasis_api_curl_defaults.h new file mode 100644 index 0000000..62e0081 --- /dev/null +++ b/src/restclient/anastasis_api_curl_defaults.h @@ -0,0 +1,38 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 + +*/ +/** + * @file lib/anastasis_api_curl_defaults.h + * @brief curl easy handle defaults + * @author Florian Dold + */ + +#ifndef _ANASTASIS_API_CURL_DEFAULTS_H +#define _ANASTASIS_API_CURL_DEFAULTS_H + +#include + + +/** + * Get a curl handle with the right defaults + * for the exchange lib. In the future, we might manage a pool of connections here. + * + * @param url URL to query + */ +CURL * +ANASTASIS_curl_easy_get_ (const char *url); + +#endif /* _TALER_CURL_DEFAULTS_H */ diff --git a/src/restclient/anastasis_api_keyshare_lookup.c b/src/restclient/anastasis_api_keyshare_lookup.c new file mode 100644 index 0000000..a2e4316 --- /dev/null +++ b/src/restclient/anastasis_api_keyshare_lookup.c @@ -0,0 +1,508 @@ +/* + 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 +*/ +/** + * @file lib/anastasis_api_keyshare_lookup.c + * @brief Implementation of the GET /truth client + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include +#include +#include /* just for HTTP status codes */ +#include "anastasis_service.h" +#include "anastasis_api_curl_defaults.h" +#include +#include + + +/** + * @brief A Contract Operation Handle + */ +struct ANASTASIS_KeyShareLookupOperation +{ + /** + * The url for this request, including parameters. + */ + char *url; + + /** + * The url for this request, without response parameter. + */ + char *display_url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + ANASTASIS_KeyShareLookupCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Identification of the Truth Object + */ + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_public_key; + + /** + * Key to decrypt the truth on the server + */ + const struct ANASTASIS_CRYPTO_TruthKeyP *truth_key; + + /** + * Hash of the response (security question) + */ + const struct GNUNET_HashCode *hashed_answer; + + /** + * Payment URI we received from the service, or NULL. + */ + char *pay_uri; + + /** + * Location URI we received from the service, or NULL. + */ + char *location; + + /** + * Content type of the body. + */ + char *content_type; +}; + + +void +ANASTASIS_keyshare_lookup_cancel ( + struct ANASTASIS_KeyShareLookupOperation *kslo) +{ + if (NULL != kslo->job) + { + GNUNET_CURL_job_cancel (kslo->job); + kslo->job = NULL; + } + GNUNET_free (kslo->location); + GNUNET_free (kslo->pay_uri); + GNUNET_free (kslo->display_url); + GNUNET_free (kslo->url); + GNUNET_free (kslo->content_type); + GNUNET_free (kslo); +} + + +/** + * Process GET /truth response + * + * @param cls our `struct ANASTASIS_KeyShareLookupOperation *` + * @param response_code the HTTP status + * @param data the body of the response + * @param data_size number of bytes in @a data + */ +static void +handle_keyshare_lookup_finished (void *cls, + long response_code, + const void *data, + size_t data_size) +{ + struct ANASTASIS_KeyShareLookupOperation *kslo = cls; + struct ANASTASIS_KeyShareDownloadDetails kdd; + + kslo->job = NULL; + memset (&kdd, + 0, + sizeof (kdd)); + kdd.server_url = kslo->display_url; + switch (response_code) + { + case 0: + /* Hard error */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Backend didn't even return from GET /truth\n"); + kdd.status = ANASTASIS_KSD_SERVER_ERROR; + break; + case MHD_HTTP_OK: + if (sizeof (struct ANASTASIS_CRYPTO_EncryptedKeyShareP) != data_size) + { + GNUNET_break_op (0); + kdd.status = ANASTASIS_KSD_SERVER_ERROR; + break; + } + /* Success, call callback with all details! */ + memcpy (&kdd.details.eks, + data, + data_size); + kslo->cb (kslo->cb_cls, + &kdd); + ANASTASIS_keyshare_lookup_cancel (kslo); + return; + case MHD_HTTP_BAD_REQUEST: + /* This should never happen, either us or the anastasis server is buggy + (or API version conflict); just pass JSON reply to the application */ + GNUNET_break (0); + kdd.status = ANASTASIS_KSD_CLIENT_FAILURE; + break; + case MHD_HTTP_PAYMENT_REQUIRED: + { + struct TALER_MERCHANT_PayUriData pd; + + if ( (NULL == kslo->pay_uri) || + (GNUNET_OK != + TALER_MERCHANT_parse_pay_uri (kslo->pay_uri, + &pd)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse `%s'\n", + kslo->pay_uri); + kdd.status = ANASTASIS_KSD_SERVER_ERROR; + break; + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + pd.order_id, + strlen (pd.order_id), + &kdd.details.payment_required.payment_secret, + sizeof (kdd.details.payment_required.payment_secret))) + { + GNUNET_break (0); + kdd.status = ANASTASIS_KSD_SERVER_ERROR; + TALER_MERCHANT_parse_pay_uri_free (&pd); + break; + } + kdd.status = ANASTASIS_KSD_PAYMENT_REQUIRED; + kdd.details.payment_required.taler_pay_uri = kslo->pay_uri; + kslo->cb (kslo->cb_cls, + &kdd); + ANASTASIS_keyshare_lookup_cancel (kslo); + TALER_MERCHANT_parse_pay_uri_free (&pd); + return; + } + break; + case MHD_HTTP_SEE_OTHER: + /* Nothing really to verify, authentication required/failed */ + kdd.status = ANASTASIS_KSD_REDIRECT_FOR_AUTHENTICATION; + kdd.details.redirect_url = kslo->location; + kslo->cb (kslo->cb_cls, + &kdd); + ANASTASIS_keyshare_lookup_cancel (kslo); + return; + case MHD_HTTP_ALREADY_REPORTED: + case MHD_HTTP_FORBIDDEN: + /* Nothing really to verify, authentication required/failed */ + { + kdd.status = ANASTASIS_KSD_INVALID_ANSWER; + kdd.details.open_challenge.body = data; + kdd.details.open_challenge.body_size = data_size; + kdd.details.open_challenge.content_type = kslo->content_type; + kdd.details.open_challenge.http_status = response_code; + kslo->cb (kslo->cb_cls, + &kdd); + } + ANASTASIS_keyshare_lookup_cancel (kslo); + return; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify */ + kdd.status = ANASTASIS_KSD_TRUTH_UNKNOWN; + break; + case MHD_HTTP_GONE: + /* Nothing really to verify */ + kdd.status = ANASTASIS_KSD_TRUTH_UNKNOWN; + break; + case MHD_HTTP_EXPECTATION_FAILED: + /* Nothing really to verify */ + kdd.status = ANASTASIS_KSD_CLIENT_FAILURE; + break; + case MHD_HTTP_TOO_MANY_REQUESTS: + kdd.status = ANASTASIS_KSD_RATE_LIMIT_EXCEEDED; + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + kdd.status = ANASTASIS_KSD_SERVER_ERROR; + kdd.details.server_failure.ec = TALER_JSON_get_error_code2 (data, + data_size); + kdd.details.server_failure.http_status = response_code; + break; + default: + /* unexpected response code */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to GET /truth\n", + (unsigned int) response_code); + GNUNET_break (0); + kdd.status = ANASTASIS_KSD_SERVER_ERROR; + kdd.details.server_failure.ec = TALER_JSON_get_error_code2 (data, + data_size); + kdd.details.server_failure.http_status = response_code; + break; + } + kslo->cb (kslo->cb_cls, + &kdd); + ANASTASIS_keyshare_lookup_cancel (kslo); +} + + +/** + * Patch value in @a val, replacing new line with '\0'. + * + * @param[in,out] 0-terminated string to replace '\n'/'\r' with '\0' in. + */ +static void +patch_value (char *val) +{ + size_t len; + + /* found location URI we care about! */ + len = strlen (val); + while ( (len > 0) && + ( ('\n' == val[len - 1]) || + ('\r' == val[len - 1]) ) ) + { + len--; + val[len] = '\0'; + } +} + + +/** + * Handle HTTP header received by curl. + * + * @param buffer one line of HTTP header data + * @param size size of an item + * @param nitems number of items passed + * @param userdata our `struct ANASTASIS_StorePolicyOperation *` + * @return `size * nitems` + */ +static size_t +handle_header (char *buffer, + size_t size, + size_t nitems, + void *userdata) +{ + struct ANASTASIS_KeyShareLookupOperation *kslo = userdata; + size_t total = size * nitems; + char *ndup; + const char *hdr_type; + char *hdr_val; + char *sp; + + ndup = GNUNET_strndup (buffer, + total); + hdr_type = strtok_r (ndup, + ":", + &sp); + if (NULL == hdr_type) + { + GNUNET_free (ndup); + return total; + } + hdr_val = strtok_r (NULL, + "", + &sp); + if (NULL == hdr_val) + { + GNUNET_free (ndup); + return total; + } + if (' ' == *hdr_val) + hdr_val++; + if (0 == strcasecmp (hdr_type, + ANASTASIS_HTTP_HEADER_TALER)) + { + /* found payment URI we care about! */ + GNUNET_free (kslo->pay_uri); + kslo->pay_uri = GNUNET_strdup (hdr_val); + patch_value (kslo->pay_uri); + } + if (0 == strcasecmp (hdr_type, + MHD_HTTP_HEADER_LOCATION)) + { + /* found location URI we care about! */ + GNUNET_free (kslo->location); + kslo->location = GNUNET_strdup (hdr_val); + patch_value (kslo->location); + } + if (0 == strcasecmp (hdr_type, + MHD_HTTP_HEADER_CONTENT_TYPE)) + { + /* found location URI we care about! */ + GNUNET_free (kslo->content_type); + kslo->content_type = GNUNET_strdup (hdr_val); + patch_value (kslo->content_type); + } + GNUNET_free (ndup); + return total; +} + + +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) +{ + struct ANASTASIS_KeyShareLookupOperation *kslo; + CURL *eh; + struct curl_slist *job_headers; + char *path; + char *answer_s; + unsigned long long tms; + + tms = (unsigned long long) (timeout.rel_value_us + / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us); + job_headers = NULL; + { + struct curl_slist *ext; + char *val; + char *hdr; + + /* Set Truth-Decryption-Key header */ + val = GNUNET_STRINGS_data_to_string_alloc (truth_key, + sizeof (*truth_key)); + GNUNET_asprintf (&hdr, + "%s: %s", + ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY, + val); + GNUNET_free (val); + ext = curl_slist_append (job_headers, + hdr); + GNUNET_free (hdr); + if (NULL == ext) + { + GNUNET_break (0); + curl_slist_free_all (job_headers); + return NULL; + } + job_headers = ext; + + /* Setup Payment-Identifier header */ + if (NULL != payment_secret) + { + char *paid_order_id; + + paid_order_id = GNUNET_STRINGS_data_to_string_alloc ( + payment_secret, + sizeof (*payment_secret)); + GNUNET_asprintf (&hdr, + "%s: %s", + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER, + paid_order_id); + GNUNET_free (paid_order_id); + ext = curl_slist_append (job_headers, + hdr); + GNUNET_free (hdr); + if (NULL == ext) + { + GNUNET_break (0); + curl_slist_free_all (job_headers); + return NULL; + } + job_headers = ext; + } + } + kslo = GNUNET_new (struct ANASTASIS_KeyShareLookupOperation); + kslo->ctx = ctx; + kslo->truth_key = truth_key; + { + char *uuid_str; + + uuid_str = GNUNET_STRINGS_data_to_string_alloc (truth_uuid, + sizeof (*truth_uuid)); + GNUNET_asprintf (&path, + "truth/%s", + uuid_str); + GNUNET_free (uuid_str); + } + { + char timeout_ms[32]; + + GNUNET_snprintf (timeout_ms, + sizeof (timeout_ms), + "%llu", + tms); + if (NULL != hashed_answer) + { + answer_s = GNUNET_STRINGS_data_to_string_alloc (hashed_answer, + sizeof (*hashed_answer)); + kslo->url = TALER_url_join (backend_url, + path, + "response", + answer_s, + "timeout_ms", + (0 != timeout.rel_value_us) + ? timeout_ms + : NULL, + NULL); + GNUNET_free (answer_s); + } + else + { + kslo->url = TALER_url_join (backend_url, + path, + "timeout_ms", + (0 != timeout.rel_value_us) + ? timeout_ms + : NULL, + NULL); + } + } + kslo->display_url = TALER_url_join (backend_url, + path, + NULL); + GNUNET_free (path); + eh = ANASTASIS_curl_easy_get_ (kslo->url); + if (0 != tms) + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_TIMEOUT_MS, + (long) (tms + 5000))); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERFUNCTION, + &handle_header)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERDATA, + kslo)); + kslo->cb = cb; + kslo->cb_cls = cb_cls; + kslo->job = GNUNET_CURL_job_add_raw (ctx, + eh, + job_headers, + &handle_keyshare_lookup_finished, + kslo); + curl_slist_free_all (job_headers); + return kslo; +} + + +/* end of anastasis_api_keyshare_lookup.c */ diff --git a/src/restclient/anastasis_api_policy_lookup.c b/src/restclient/anastasis_api_policy_lookup.c new file mode 100644 index 0000000..1af95d7 --- /dev/null +++ b/src/restclient/anastasis_api_policy_lookup.c @@ -0,0 +1,356 @@ +/* + This file is part of ANASTASIS + Copyright (C) 2014-2019 GNUnet e.V. and INRIA + + 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 2.1, + 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.LGPL. If not, + see +*/ + +/** + * @file lib/anastasis_api_policy_lookup.c + * @brief Implementation of the /policy GET and POST + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include +#include +#include /* just for HTTP status codes */ +#include "anastasis_service.h" +#include "anastasis_api_curl_defaults.h" +#include + + +/** + * @brief A Contract Operation Handle + */ +struct ANASTASIS_PolicyLookupOperation +{ + + /** + * The url for this request, including parameters. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + ANASTASIS_PolicyLookupCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Public key of the account we are downloading from. + */ + struct ANASTASIS_CRYPTO_AccountPublicKeyP account_pub; + + /** + * Signature returned in the #ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE + * header, or all zeros for none. + */ + struct ANASTASIS_AccountSignatureP account_sig; + + /** + * Version of the policy. + */ + unsigned int version; + +}; + + +void +ANASTASIS_policy_lookup_cancel (struct ANASTASIS_PolicyLookupOperation *plo) +{ + if (NULL != plo->job) + { + GNUNET_CURL_job_cancel (plo->job); + plo->job = NULL; + } + GNUNET_free (plo->url); + GNUNET_free (plo); +} + + +/** + * Process GET /policy response + */ +static void +handle_policy_lookup_finished (void *cls, + long response_code, + const void *data, + size_t data_size) +{ + struct ANASTASIS_PolicyLookupOperation *plo = cls; + + plo->job = NULL; + switch (response_code) + { + case 0: + /* Hard error */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Backend didn't even return from GET /policy\n"); + break; + case MHD_HTTP_OK: + { + struct ANASTASIS_DownloadDetails dd; + struct ANASTASIS_UploadSignaturePS usp = { + .purpose.purpose = htonl (TALER_SIGNATURE_ANASTASIS_POLICY_UPLOAD), + .purpose.size = htonl (sizeof (usp)), + }; + + GNUNET_CRYPTO_hash (data, + data_size, + &usp.new_recovery_data_hash); + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_ANASTASIS_POLICY_UPLOAD, + &usp, + &plo->account_sig.eddsa_sig, + &plo->account_pub.pub)) + { + GNUNET_break_op (0); + response_code = 0; + break; + } + /* Success, call callback with all details! */ + memset (&dd, 0, sizeof (dd)); + dd.sig = plo->account_sig; + dd.curr_policy_hash = usp.new_recovery_data_hash; + dd.policy = data; + dd.policy_size = data_size; + dd.version = plo->version; + plo->cb (plo->cb_cls, + response_code, + &dd); + plo->cb = NULL; + ANASTASIS_policy_lookup_cancel (plo); + return; + } + case MHD_HTTP_BAD_REQUEST: + /* This should never happen, either us or the anastasis server is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify */ + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u\n", + (unsigned int) response_code); + GNUNET_break (0); + response_code = 0; + break; + } + plo->cb (plo->cb_cls, + response_code, + NULL); + plo->cb = NULL; + ANASTASIS_policy_lookup_cancel (plo); +} + + +/** + * Handle HTTP header received by curl. + * + * @param buffer one line of HTTP header data + * @param size size of an item + * @param nitems number of items passed + * @param userdata our `struct ANASTASIS_PolicyLookupOperation *` + * @return `size * nitems` + */ +static size_t +handle_header (char *buffer, + size_t size, + size_t nitems, + void *userdata) +{ + struct ANASTASIS_PolicyLookupOperation *plo = userdata; + size_t total = size * nitems; + char *ndup; + const char *hdr_type; + char *hdr_val; + char *sp; + + ndup = GNUNET_strndup (buffer, + total); + hdr_type = strtok_r (ndup, + ":", + &sp); + if (NULL == hdr_type) + { + GNUNET_free (ndup); + return total; + } + hdr_val = strtok_r (NULL, + "\n\r", + &sp); + if (NULL == hdr_val) + { + GNUNET_free (ndup); + return total; + } + if (' ' == *hdr_val) + hdr_val++; + if (0 == strcasecmp (hdr_type, + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE)) + { + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + hdr_val, + strlen (hdr_val), + &plo->account_sig, + sizeof (struct ANASTASIS_AccountSignatureP))) + { + GNUNET_break_op (0); + GNUNET_free (ndup); + return 0; + } + } + if (0 == strcasecmp (hdr_type, + ANASTASIS_HTTP_HEADER_POLICY_VERSION)) + { + char dummy; + + if (1 != + sscanf (hdr_val, + "%u%c", + &plo->version, + &dummy)) + { + GNUNET_break_op (0); + GNUNET_free (ndup); + return 0; + } + } + GNUNET_free (ndup); + return total; +} + + +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) +{ + struct ANASTASIS_PolicyLookupOperation *plo; + CURL *eh; + char *acc_pub_str; + char *path; + + GNUNET_assert (NULL != cb); + plo = GNUNET_new (struct ANASTASIS_PolicyLookupOperation); + plo->account_pub = *anastasis_pub; + acc_pub_str = GNUNET_STRINGS_data_to_string_alloc (anastasis_pub, + sizeof (*anastasis_pub)); + GNUNET_asprintf (&path, + "policy/%s", + acc_pub_str); + GNUNET_free (acc_pub_str); + plo->url = TALER_url_join (backend_url, + path, + NULL); + GNUNET_free (path); + eh = ANASTASIS_curl_easy_get_ (plo->url); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERFUNCTION, + &handle_header)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERDATA, + plo)); + plo->cb = cb; + plo->cb_cls = cb_cls; + plo->job = GNUNET_CURL_job_add_raw (ctx, + eh, + GNUNET_NO, + &handle_policy_lookup_finished, + plo); + return plo; +} + + +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) +{ + struct ANASTASIS_PolicyLookupOperation *plo; + CURL *eh; + char *acc_pub_str; + char *path; + char version_s[14]; + + GNUNET_assert (NULL != cb); + plo = GNUNET_new (struct ANASTASIS_PolicyLookupOperation); + plo->account_pub = *anastasis_pub; + acc_pub_str = GNUNET_STRINGS_data_to_string_alloc (anastasis_pub, + sizeof (*anastasis_pub)); + GNUNET_asprintf (&path, + "policy/%s", + acc_pub_str); + GNUNET_free (acc_pub_str); + GNUNET_snprintf (version_s, + sizeof (version_s), + "%u", + version); + plo->url = TALER_url_join (backend_url, + path, + "version", + version_s, + NULL); + GNUNET_free (path); + eh = ANASTASIS_curl_easy_get_ (plo->url); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERFUNCTION, + &handle_header)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERDATA, + plo)); + plo->cb = cb; + plo->cb_cls = cb_cls; + plo->job = GNUNET_CURL_job_add_raw (ctx, + eh, + GNUNET_NO, + &handle_policy_lookup_finished, + plo); + return plo; +} diff --git a/src/restclient/anastasis_api_policy_store.c b/src/restclient/anastasis_api_policy_store.c new file mode 100644 index 0000000..7c6c244 --- /dev/null +++ b/src/restclient/anastasis_api_policy_store.c @@ -0,0 +1,527 @@ +/* + This file is part of ANASTASIS + Copyright (C) 2014-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 2.1, + 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.LGPL. If not, + see +*/ + +/** + * @file lib/anastasis_api_policy_store.c + * @brief Implementation of the /policy GET and POST + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include +#include /* just for HTTP status codes */ +#include "anastasis_service.h" +#include "anastasis_api_curl_defaults.h" +#include +#include +#include + + +struct ANASTASIS_PolicyStoreOperation +{ + /** + * Complete URL where the backend offers /policy + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * The CURL context to connect to the backend + */ + struct GNUNET_CURL_Context *ctx; + + /** + * The callback to pass the backend response to + */ + ANASTASIS_PolicyStoreCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Payment URI we received from the service, or NULL. + */ + char *pay_uri; + + /** + * Policy version we received from the service, or NULL. + */ + char *policy_version; + + /** + * Policy expiration we received from the service, or NULL. + */ + char *policy_expiration; + + /** + * Copy of the uploaded data. Needed by curl. + */ + void *postcopy; + + /** + * Hash of the data we are uploading. + */ + struct GNUNET_HashCode new_upload_hash; +}; + + +void +ANASTASIS_policy_store_cancel ( + struct ANASTASIS_PolicyStoreOperation *pso) +{ + if (NULL != pso->job) + { + GNUNET_CURL_job_cancel (pso->job); + pso->job = NULL; + } + GNUNET_free (pso->policy_version); + GNUNET_free (pso->policy_expiration); + GNUNET_free (pso->pay_uri); + GNUNET_free (pso->url); + GNUNET_free (pso->postcopy); + GNUNET_free (pso); +} + + +/** + * Callback to process POST /policy response + * + * @param cls the `struct ANASTASIS_PolicyStoreOperation` + * @param response_code HTTP response code, 0 on error + * @param data response body + * @param data_size number of bytes in @a data + */ +static void +handle_policy_store_finished (void *cls, + long response_code, + const void *data, + size_t data_size) +{ + struct ANASTASIS_PolicyStoreOperation *pso = cls; + struct ANASTASIS_UploadDetails ud; + + pso->job = NULL; + memset (&ud, 0, sizeof (ud)); + ud.http_status = response_code; + ud.ec = TALER_EC_NONE; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Policy store finished with HTTP status %u\n", + (unsigned int) response_code); + switch (response_code) + { + case 0: + ud.us = ANASTASIS_US_SERVER_ERROR; + ud.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_NO_CONTENT: + case MHD_HTTP_NOT_MODIFIED: + { + unsigned long long version; + unsigned long long expiration; + char dummy; + + if (1 != sscanf (pso->policy_version, + "%llu%c", + &version, + &dummy)) + { + ud.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + ud.us = ANASTASIS_US_SERVER_ERROR; + break; + } + if (1 != sscanf (pso->policy_expiration, + "%llu%c", + &expiration, + &dummy)) + { + ud.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + ud.us = ANASTASIS_US_SERVER_ERROR; + break; + } + ud.us = ANASTASIS_US_SUCCESS; + ud.details.success.curr_backup_hash = &pso->new_upload_hash; + ud.details.success.policy_expiration + = GNUNET_TIME_absolute_add ( + GNUNET_TIME_UNIT_ZERO_ABS, + GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_SECONDS, + expiration)); + ud.details.success.policy_version = version; + } + break; + case MHD_HTTP_BAD_REQUEST: + GNUNET_break (0); + ud.us = ANASTASIS_US_CLIENT_ERROR; + ud.ec = TALER_JSON_get_error_code2 (data, + data_size); + break; + case MHD_HTTP_PAYMENT_REQUIRED: + { + struct TALER_MERCHANT_PayUriData pd; + + if ( (NULL == pso->pay_uri) || + (GNUNET_OK != + TALER_MERCHANT_parse_pay_uri (pso->pay_uri, + &pd)) ) + { + GNUNET_break_op (0); + ud.ec = TALER_EC_ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST; + break; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Policy store operation requires payment `%s'\n", + pso->pay_uri); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + pd.order_id, + strlen (pd.order_id), + &ud.details.payment.ps, + sizeof (ud.details.payment.ps))) + { + GNUNET_break (0); + ud.ec = TALER_EC_ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST; + TALER_MERCHANT_parse_pay_uri_free (&pd); + break; + } + TALER_MERCHANT_parse_pay_uri_free (&pd); + } + ud.us = ANASTASIS_US_PAYMENT_REQUIRED; + ud.details.payment.payment_request = pso->pay_uri; + break; + case MHD_HTTP_PAYLOAD_TOO_LARGE: + ud.us = ANASTASIS_US_CLIENT_ERROR; + ud.ec = TALER_EC_GENERIC_UPLOAD_EXCEEDS_LIMIT; + break; + case MHD_HTTP_LENGTH_REQUIRED: + GNUNET_break (0); + ud.ec = TALER_JSON_get_error_code2 (data, + data_size); + ud.us = ANASTASIS_US_SERVER_ERROR; + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + ud.ec = TALER_JSON_get_error_code2 (data, + data_size); + ud.us = ANASTASIS_US_SERVER_ERROR; + break; + default: + ud.ec = TALER_JSON_get_error_code2 (data, + data_size); + ud.us = ANASTASIS_US_SERVER_ERROR; + break; + } + pso->cb (pso->cb_cls, + &ud); + pso->cb = NULL; + ANASTASIS_policy_store_cancel (pso); +} + + +/** + * Handle HTTP header received by curl. + * + * @param buffer one line of HTTP header data + * @param size size of an item + * @param nitems number of items passed + * @param userdata our `struct ANASTASIS_StorePolicyOperation *` + * @return `size * nitems` + */ +static size_t +handle_header (char *buffer, + size_t size, + size_t nitems, + void *userdata) +{ + struct ANASTASIS_PolicyStoreOperation *pso = userdata; + size_t total = size * nitems; + char *ndup; + const char *hdr_type; + char *hdr_val; + char *sp; + + ndup = GNUNET_strndup (buffer, + total); + hdr_type = strtok_r (ndup, + ":", + &sp); + if (NULL == hdr_type) + { + GNUNET_free (ndup); + return total; + } + hdr_val = strtok_r (NULL, + "", + &sp); + if (NULL == hdr_val) + { + GNUNET_free (ndup); + return total; + } + if (' ' == *hdr_val) + hdr_val++; + if (0 == strcasecmp (hdr_type, + "Taler")) + { + size_t len; + + /* found payment URI we care about! */ + GNUNET_free (pso->pay_uri); /* In case of duplicate header */ + pso->pay_uri = GNUNET_strdup (hdr_val); + len = strlen (pso->pay_uri); + while ( (len > 0) && + ( ('\n' == pso->pay_uri[len - 1]) || + ('\r' == pso->pay_uri[len - 1]) ) ) + { + len--; + pso->pay_uri[len] = '\0'; + } + } + + if (0 == strcasecmp (hdr_type, + ANASTASIS_HTTP_HEADER_POLICY_VERSION)) + { + size_t len; + + /* found policy version we care about! */ + GNUNET_free (pso->policy_version); /* In case of duplicate header */ + pso->policy_version = GNUNET_strdup (hdr_val); + len = strlen (pso->policy_version); + while ( (len > 0) && + ( ('\n' == pso->policy_version[len - 1]) || + ('\r' == pso->policy_version[len - 1]) ) ) + { + len--; + pso->policy_version[len] = '\0'; + } + } + + if (0 == strcasecmp (hdr_type, + ANASTASIS_HTTP_HEADER_POLICY_EXPIRATION)) + { + size_t len; + + /* found policy expiration we care about! */ + GNUNET_free (pso->policy_expiration); /* In case of duplicate header */ + pso->policy_expiration = GNUNET_strdup (hdr_val); + len = strlen (pso->policy_expiration); + while ( (len > 0) && + ( ('\n' == pso->policy_expiration[len - 1]) || + ('\r' == pso->policy_expiration[len - 1]) ) ) + { + len--; + pso->policy_expiration[len] = '\0'; + } + } + + GNUNET_free (ndup); + return total; +} + + +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) +{ + struct ANASTASIS_PolicyStoreOperation *pso; + struct ANASTASIS_AccountSignatureP account_sig; + unsigned long long tms; + CURL *eh; + struct curl_slist *job_headers; + struct ANASTASIS_UploadSignaturePS usp = { + .purpose.purpose = htonl (TALER_SIGNATURE_ANASTASIS_POLICY_UPLOAD), + .purpose.size = htonl (sizeof (usp)) + }; + + tms = (unsigned long long) (payment_timeout.rel_value_us + / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us); + GNUNET_CRYPTO_hash (recovery_data, + recovery_data_size, + &usp.new_recovery_data_hash); + GNUNET_CRYPTO_eddsa_sign (&anastasis_priv->priv, + &usp, + &account_sig.eddsa_sig); + /* setup our HTTP headers */ + job_headers = NULL; + { + struct curl_slist *ext; + char *val; + char *hdr; + + /* Set Anastasis-Policy-Signature header */ + val = GNUNET_STRINGS_data_to_string_alloc (&account_sig, + sizeof (account_sig)); + GNUNET_asprintf (&hdr, + "%s: %s", + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE, + val); + GNUNET_free (val); + ext = curl_slist_append (job_headers, + hdr); + GNUNET_free (hdr); + if (NULL == ext) + { + GNUNET_break (0); + curl_slist_free_all (job_headers); + return NULL; + } + job_headers = ext; + + /* set Etag header */ + val = GNUNET_STRINGS_data_to_string_alloc (&usp.new_recovery_data_hash, + sizeof (struct GNUNET_HashCode)); + GNUNET_asprintf (&hdr, + "%s: %s", + MHD_HTTP_HEADER_IF_NONE_MATCH, + val); + GNUNET_free (val); + ext = curl_slist_append (job_headers, + hdr); + GNUNET_free (hdr); + if (NULL == ext) + { + GNUNET_break (0); + curl_slist_free_all (job_headers); + return NULL; + } + job_headers = ext; + + /* Setup Payment-Identifier header */ + if (NULL != payment_secret) + { + char *paid_order_id; + + paid_order_id = GNUNET_STRINGS_data_to_string_alloc ( + payment_secret, + sizeof (*payment_secret)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Beginning policy store operation with payment secret `%s'\n", + paid_order_id); + GNUNET_asprintf (&hdr, + "%s: %s", + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER, + paid_order_id); + GNUNET_free (paid_order_id); + ext = curl_slist_append (job_headers, + hdr); + GNUNET_free (hdr); + if (NULL == ext) + { + GNUNET_break (0); + curl_slist_free_all (job_headers); + return NULL; + } + job_headers = ext; + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Beginning policy store operation without payment secret\n"); + } + } + /* Finished setting up headers */ + pso = GNUNET_new (struct ANASTASIS_PolicyStoreOperation); + pso->postcopy = GNUNET_memdup (recovery_data, + recovery_data_size); + pso->new_upload_hash = usp.new_recovery_data_hash; + { + char *acc_pub_str; + char *path; + struct ANASTASIS_CRYPTO_AccountPublicKeyP pub; + char timeout_ms[32]; + char pyrs[32]; + + GNUNET_snprintf (timeout_ms, + sizeof (timeout_ms), + "%llu", + tms); + GNUNET_snprintf (pyrs, + sizeof (pyrs), + "%u", + (unsigned int) payment_years_requested); + GNUNET_CRYPTO_eddsa_key_get_public (&anastasis_priv->priv, + &pub.pub); + acc_pub_str + = GNUNET_STRINGS_data_to_string_alloc (&pub, + sizeof (pub)); + GNUNET_asprintf (&path, + "policy/%s", + acc_pub_str); + GNUNET_free (acc_pub_str); + pso->url = TALER_url_join (backend_url, + path, + "storage_duration", + (0 != payment_years_requested) + ? pyrs + : NULL, + "timeout_ms", + (0 != payment_timeout.rel_value_us) + ? timeout_ms + : NULL, + NULL); + GNUNET_free (path); + } + pso->ctx = ctx; + pso->cb = cb; + pso->cb_cls = cb_cls; + eh = ANASTASIS_curl_easy_get_ (pso->url); + if (0 != tms) + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_TIMEOUT_MS, + (long) (tms + 5000))); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDS, + pso->postcopy)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDSIZE, + (long) recovery_data_size)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERFUNCTION, + &handle_header)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERDATA, + pso)); + pso->job = GNUNET_CURL_job_add_raw (ctx, + eh, + job_headers, + &handle_policy_store_finished, + pso); + curl_slist_free_all (job_headers); + return pso; +} diff --git a/src/restclient/anastasis_api_truth_store.c b/src/restclient/anastasis_api_truth_store.c new file mode 100644 index 0000000..ebd7d10 --- /dev/null +++ b/src/restclient/anastasis_api_truth_store.c @@ -0,0 +1,354 @@ +/* + 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 +*/ +/** + * @file lib/anastasis_api_truth_store.c + * @brief Implementation of the /truth GET and POST + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include +#include +#include /* just for HTTP status codes */ +#include "anastasis_service.h" +#include "anastasis_api_curl_defaults.h" +#include +#include + + +struct ANASTASIS_TruthStoreOperation +{ + /** + * Complete URL where the backend offers /truth + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * The CURL context to connect to the backend + */ + struct GNUNET_CURL_Context *ctx; + + /** + * The callback to pass the backend response to + */ + ANASTASIS_TruthStoreCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Reference to data (for cleanup). + */ + char *data; + + /** + * Payment URI we received from the service, or NULL. + */ + char *pay_uri; +}; + + +void +ANASTASIS_truth_store_cancel ( + struct ANASTASIS_TruthStoreOperation *tso) +{ + if (NULL != tso->job) + { + GNUNET_CURL_job_cancel (tso->job); + tso->job = NULL; + } + GNUNET_free (tso->pay_uri); + GNUNET_free (tso->url); + GNUNET_free (tso->data); + GNUNET_free (tso); +} + + +/** + * Callback to process POST /truth response + * + * @param cls the `struct ANASTASIS_TruthStoreOperation` + * @param response_code HTTP response code, 0 on error + * @param data + * @param data_size + */ +static void +handle_truth_store_finished (void *cls, + long response_code, + const void *data, + size_t data_size) +{ + struct ANASTASIS_TruthStoreOperation *tso = cls; + struct ANASTASIS_UploadDetails ud; + + tso->job = NULL; + memset (&ud, 0, sizeof (ud)); + ud.http_status = response_code; + ud.ec = TALER_EC_NONE; + switch (response_code) + { + case 0: + break; + case MHD_HTTP_NO_CONTENT: + ud.us = ANASTASIS_US_SUCCESS; + break; + case MHD_HTTP_NOT_MODIFIED: + ud.us = ANASTASIS_US_SUCCESS; + break; + case MHD_HTTP_BAD_REQUEST: + GNUNET_break (0); + ud.ec = TALER_JSON_get_error_code2 (data, + data_size); + break; + case MHD_HTTP_PAYMENT_REQUIRED: + { + struct TALER_MERCHANT_PayUriData pd; + + if ( (NULL == tso->pay_uri) || + (GNUNET_OK != + TALER_MERCHANT_parse_pay_uri (tso->pay_uri, + &pd)) ) + { + GNUNET_break_op (0); + ud.ec = TALER_EC_ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST; + break; + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + pd.order_id, + strlen (pd.order_id), + &ud.details.payment.ps, + sizeof (ud.details.payment.ps))) + { + GNUNET_break (0); + ud.ec = TALER_EC_ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST; + TALER_MERCHANT_parse_pay_uri_free (&pd); + break; + } + TALER_MERCHANT_parse_pay_uri_free (&pd); + } + ud.us = ANASTASIS_US_PAYMENT_REQUIRED; + ud.details.payment.payment_request = tso->pay_uri; + break; + case MHD_HTTP_CONFLICT: + ud.us = ANASTASIS_US_CONFLICTING_TRUTH; + break; + case MHD_HTTP_LENGTH_REQUIRED: + GNUNET_break (0); + break; + case MHD_HTTP_REQUEST_ENTITY_TOO_LARGE: + ud.ec = TALER_JSON_get_error_code2 (data, + data_size); + break; + case MHD_HTTP_TOO_MANY_REQUESTS: + ud.ec = TALER_JSON_get_error_code2 (data, + data_size); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + ud.ec = TALER_JSON_get_error_code2 (data, + data_size); + break; + default: + GNUNET_break (0); + ud.ec = TALER_JSON_get_error_code2 (data, + data_size); + break; + } + tso->cb (tso->cb_cls, + &ud); + tso->cb = NULL; + ANASTASIS_truth_store_cancel (tso); +} + + +/** + * Handle HTTP header received by curl. + * + * @param buffer one line of HTTP header data + * @param size size of an item + * @param nitems number of items passed + * @param userdata our `struct ANASTASIS_StorePolicyOperation *` + * @return `size * nitems` + */ +static size_t +handle_header (char *buffer, + size_t size, + size_t nitems, + void *userdata) +{ + struct ANASTASIS_TruthStoreOperation *tso = userdata; + size_t total = size * nitems; + char *ndup; + const char *hdr_type; + char *hdr_val; + char *sp; + + ndup = GNUNET_strndup (buffer, + total); + hdr_type = strtok_r (ndup, + ":", + &sp); + if (NULL == hdr_type) + { + GNUNET_free (ndup); + return total; + } + hdr_val = strtok_r (NULL, + "", + &sp); + if (NULL == hdr_val) + { + GNUNET_free (ndup); + return total; + } + if (' ' == *hdr_val) + hdr_val++; + if (0 == strcasecmp (hdr_type, + ANASTASIS_HTTP_HEADER_TALER)) + { + size_t len; + + /* found payment URI we care about! */ + tso->pay_uri = GNUNET_strdup (hdr_val); + len = strlen (tso->pay_uri); + while ( (len > 0) && + ( ('\n' == tso->pay_uri[len - 1]) || + ('\r' == tso->pay_uri[len - 1]) ) ) + { + len--; + tso->pay_uri[len] = '\0'; + } + } + GNUNET_free (ndup); + return total; +} + + +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) +{ + struct ANASTASIS_TruthStoreOperation *tso; + CURL *eh; + char *json_str; + unsigned long long tms; + + tms = (unsigned long long) (payment_timeout.rel_value_us + / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us); + tso = GNUNET_new (struct ANASTASIS_TruthStoreOperation); + { + char *uuid_str; + char *path; + char timeout_ms[32]; + + GNUNET_snprintf (timeout_ms, + sizeof (timeout_ms), + "%llu", + tms); + uuid_str = GNUNET_STRINGS_data_to_string_alloc (uuid, + sizeof (*uuid)); + GNUNET_asprintf (&path, + "truth/%s", + uuid_str); + tso->url = TALER_url_join (backend_url, + path, + "timeout_ms", + (0 != payment_timeout.rel_value_us) + ? timeout_ms + : NULL, + NULL); + GNUNET_free (path); + GNUNET_free (uuid_str); + } + { + json_t *truth_data; + + truth_data = json_pack ("{s:o," /* encrypted KeyShare */ + " s:s," /* type */ + " s:o," /* nonce */ + " s:s," /* truth_mime */ + " s:I}", /* payment years */ + "keyshare_data", + GNUNET_JSON_from_data_auto (encrypted_keyshare), + "type", + type, + "encrypted_truth", + GNUNET_JSON_from_data (encrypted_truth, + encrypted_truth_size), + "truth_mime", + (NULL != truth_mime) + ? truth_mime + : "", + "storage_duration_years", + (json_int_t) payment_years_requested); + GNUNET_assert (NULL != truth_data); + json_str = json_dumps (truth_data, + JSON_COMPACT); + GNUNET_assert (NULL != json_str); + json_decref (truth_data); + } + tso->ctx = ctx; + tso->data = json_str; + tso->cb = cb; + tso->cb_cls = cb_cls; + eh = ANASTASIS_curl_easy_get_ (tso->url); + if (0 != tms) + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_TIMEOUT_MS, + (long) (tms + 5000))); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDS, + json_str)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDSIZE, + strlen (json_str))); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERFUNCTION, + &handle_header)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERDATA, + tso)); + tso->job = GNUNET_CURL_job_add_raw (ctx, + eh, + GNUNET_NO, + &handle_truth_store_finished, + tso); + return tso; +} diff --git a/src/stasis/Datenbank-Schema.xml b/src/stasis/Datenbank-Schema.xml new file mode 100644 index 0000000..0591d22 --- /dev/null +++ b/src/stasis/Datenbank-Schema.xml @@ -0,0 +1 @@ +7V1bc9o4GP01zOw+bMdXcB6B0sts0mYSdrd9YhRbwZ7aFiOLBPrrV7IlX5BpXGyDmVGbmaCrLX1HR0f6JDIy59HuIwYb/w55MBwZmrcbme9HhqHrmkF/sZh9FuPYWhaxxoHHMxURj8FPyCNFtm3gwaSSkSAUkmBTjXRRHEOXVOIAxui1mu0ZhdWnbsAaShGPLgjl2P8Cj/iiXeObIuETDNY+f7RjTLKECIjMvCWJDzz0WooyFyNzjhEi2adoN4ch6zzRL1m5D0dS8xfDMCZNCnBDvIBwy9v2TwIxfzeyFw1OXoMoBDENzZ5RTB55ikbDrh+E3i3Yoy17YEKA+0OEZj7CwU+aH4Q0SacRNBkTbk9jzGoLwnCOQoTT55hQY/8rJR9ZjfxZGCa07L1onX4QdQd2lYy3ICHiLVEYgk0SPKXvzQpGAK+DeIYIQRHPJFr5ofpSz+k/mg7CYB3TOJc+i3YS74usNbpFw3Lvc4O8QEzgrhTFrfERoggSvKdZeKp1w5HBh4ah8SpeC6Dl48CvYIzjm2N7nVddmJ9+4AioR4MpoWFL0bBiydPZ9+ViKgPDBxv2kbaZBCB8oKMNxGuWOiNow/s1hM/CDpi/MPv8JPpeL/VtlnfGOiygA27Ko6PA89Jay4CJUYrIZAPcIF7fZk8xrSLqgT+NRSFa5XOYjjWfVgZj9jaIAAKecixvUBCTtP/sGf2hvTzX3tkjm7Z0TsN6EaY/LDsmcxQnBIMgNTikgHuFDHQzD6PNkkIMitaWx419gB2jFjtGY+wIrIybQcXU2kPFkqBy/3drdDyVR2ONpZuCpIqIzgDxls1ZMzmc37auecS6+azFX6MyMVSsXjKz2ZOVdZkR4G4TYEACFK88QGjkdPn5bvG4nN7dX4wd0squnBq6pQK7IRUIymgFEpkLLs0EGSCugQb03nmgExPbkomXeEufqoTipYSioVWFol43+9s1eNCdLgAxlgBBGCAyrfjv9GH+afrwx9j6UynGMyrGfJi2kox1oOlETEyUZmw5WYyP2HdQorFmbQBjF+83BHqrH3C/oibGsFhS0kaZupY3uGuu0JR+PIEY6gRkHTF0oS4MWV1cmhauR0Dmo23YAtKRTAyoXKCtoL2eLiZplT4qiQdFBsMhA7G3eA4y0G8UGZwuEJwjph0UGQjnh7R4iAJa156ZVrHA8FhA15wzagJdwoiigcaaQDti22HRgOx8LNYJB+rAo13XrxNKccJJnGBaZ+QE2RuhOKExJxzzIQyKE0xZGhQOp/59TYoETiKBsXY+EjCVMGjhdL4KYWDKwoCakHXd3kPuNmLNUY6nSzmeJpOq48m0a0Z/na9Zd6wOsPGrM0oVv9O8tLNMW5dkE8jnL8t+pg7lkqpnnGOyo/UpJrsLNKljTG1nlN7PL3Rh57HsesR8RjlYWpacUcov1bvW/H12cBpONl3okLHstLg0N1yP1MyH3KCl5kSeACrEsPJB4it2uBZ20LUznnucKLf16fQwuQq39UQ+5nawlFCMMHBGEEXOwgiyzFSM0JgRej/c1omJaw6yuC7axmSVBGulFK6GF+waZ3ZvvKDOtLTghas402LJbol7sGdb1YlsarVXPbrMXrV103DQd3JFUnZjbDJIZLvVaiv6vFvRVnP4XOJCrezYUFvRvzVR5ONt0LcjbHlJeZZL9komnsIA57xHa8urxw8XZ4Dr0Yn2VawfLXnDeYMSskqXkEwG9ScLFAOcwgB1dx/6YgBL7Si3mP6vYkfZkqd/sShIoIsZlJUKGBoH1N586I0E1CZyCxK4ChEwlnmetjxOgJtediBBBNVZ54FSQd2Fh94OoMiThaKCxgdQjp1jHxQViMOzXZpYDffuhnvd1Ya64T7pYmtIXW1osfzv/WpDFyYeyz4BycTQW0Oxs82uQJL9AwzTC1CLIkXqEZ9Ewi8EY2/KvuWWBhcP1MxLdAfiPUvYBeSbyEQ/f2dmeWfz0Hvh70kDexGIaRO/lQOlUixYFEtDopxsIW6NBG2xy5vK97yJGMajYhuH9cIvR2jJWrpWY648Eqe99wIrb1FnRP6MewbSUemLtrISYkfQOoBB1h5eqECCVI9+UJF1WFHWC1JFKaTydjfzNzcgkh5RJhDzl/ZO0+wqbEzTfAM4aegeYvZdAGxDLIfk7yL3KALLcBPn1wcDt8nB97pJtNMUb4fAdfTe8ObIIgZE6ZmYF+asntKkz1+WjhI2vd3ZbKxrjjjGjXMenHWUzjld5zi965xOTCzrHM4Izxi4OSUoRhguI5zz4KyjvtmhBSMYR2w9LEaocXyC9NADjZx9/Xq7mH5RdDBcOhCnE85CB8oP2oIOhugHpcHij9pkK4ziTwOZi/8B \ No newline at end of file diff --git a/src/stasis/Makefile.am b/src/stasis/Makefile.am new file mode 100644 index 0000000..a1d6584 --- /dev/null +++ b/src/stasis/Makefile.am @@ -0,0 +1,95 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include + +plugindir = $(libdir)/anastasis + +if HAVE_POSTGRESQL +plugin_LTLIBRARIES = \ + libanastasis_plugin_db_postgres.la +endif + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +sqldir = $(prefix)/share/anastasis/sql/ + +sql_DATA = \ + stasis-0000.sql \ + stasis-0001.sql \ + drop0001.sql + +pkgcfgdir = $(prefix)/share/anastasis/config.d/ + +pkgcfg_DATA = \ + stasis-postgres.conf + +bin_PROGRAMS = \ + anastasis-dbinit + +anastasis_dbinit_SOURCES = \ + anastasis-dbinit.c + +anastasis_dbinit_LDADD = \ + $(LIBGCRYPT_LIBS) \ + libanastasisdb.la \ + $(top_builddir)/src/util/libanastasisutil.la \ + -lgnunetutil \ + -ltalerutil \ + -ltalerpq \ + $(XLIB) + + +lib_LTLIBRARIES = \ + libanastasisdb.la + +libanastasisdb_la_SOURCES = \ + anastasis_db_plugin.c +libanastasisdb_la_LIBADD = \ + -lgnunetpq \ + -lpq \ + -lgnunetutil \ + -lltdl \ + $(XLIB) +libanastasisdb_la_LDFLAGS = \ + $(POSTGRESQL_LDFLAGS) \ + -version-info 2:0:0 \ + -no-undefined + +libanastasis_plugin_db_postgres_la_SOURCES = \ + plugin_anastasis_postgres.c +libanastasis_plugin_db_postgres_la_LIBADD = \ + $(LTLIBINTL) +libanastasis_plugin_db_postgres_la_LDFLAGS = \ + $(top_builddir)/src/util/libanastasisutil.la \ + $(ANASTASIS_PLUGIN_LDFLAGS) \ + -lgnunetpq \ + -lpq \ + -ltalerpq \ + -ltalerutil \ + -lgnunetutil \ + $(XLIB) + +check_PROGRAMS = \ + $(TESTS) + +test_anastasis_db_postgres_SOURCES = \ + test_anastasis_db.c +test_anastasis_db_postgres_LDFLAGS = \ + $(top_builddir)/src/util/libanastasisutil.la \ + libanastasisdb.la \ + -lgnunetutil \ + -lgnunetpq \ + -ltalerutil \ + -ltalerpq \ + -luuid \ + $(XLIB) + +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 = \ + test_anastasis_db-postgres + +EXTRA_DIST = \ + test_anastasis_db_postgres.conf \ + $(sql_DATA) diff --git a/src/stasis/anastasis-dbinit.c b/src/stasis/anastasis-dbinit.c new file mode 100644 index 0000000..5c0a174 --- /dev/null +++ b/src/stasis/anastasis-dbinit.c @@ -0,0 +1,112 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 GNUnet e.V. + + 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 +*/ +/** + * @file util/anastasis-dbinit.c + * @brief Create tables for the merchant database. + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_database_lib.h" + + +/** + * Return value from main(). + */ +static int global_ret; + +/** + * -r option: do full DB reset + */ +static int reset_db; + +/** + * Main function that will be run. + * + * @param cls closure + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be NULL!) + * @param cfg configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + struct ANASTASIS_DatabasePlugin *plugin; + + if (NULL == + (plugin = ANASTASIS_DB_plugin_load (cfg))) + { + fprintf (stderr, + "Failed to initialize database plugin.\n"); + global_ret = 1; + return; + } + if (reset_db) + { + (void) plugin->drop_tables (plugin->cls); + ANASTASIS_DB_plugin_unload (plugin); + plugin = ANASTASIS_DB_plugin_load (cfg); + } + ANASTASIS_DB_plugin_unload (plugin); +} + + +/** + * The main function of the database initialization tool. + * Used to initialize the Anastasis' database. + * + * @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) +{ + struct GNUNET_GETOPT_CommandLineOption options[] = { + + GNUNET_GETOPT_option_flag ('r', + "reset", + "reset database (DANGEROUS: all existing data is lost!)", + &reset_db), + + GNUNET_GETOPT_OPTION_END + }; + + /* force linker to link against libtalerutil; if we do + not do this, the linker may "optimize" libtalerutil + away and skip #TALER_OS_init(), which we do need */ + (void) TALER_project_data_default (); + GNUNET_OS_init (ANASTASIS_project_data_default ()); + GNUNET_assert (GNUNET_OK == + GNUNET_log_setup ("anastasis-dbinit", + "INFO", + NULL)); + if (GNUNET_OK != + GNUNET_PROGRAM_run (argc, argv, + "anastasis-dbinit", + "Initialize anastasis database", + options, + &run, NULL)) + return 1; + return global_ret; +} + + +/* end of anastasis-dbinit.c */ diff --git a/src/stasis/anastasis_db_plugin.c b/src/stasis/anastasis_db_plugin.c new file mode 100644 index 0000000..6b5332c --- /dev/null +++ b/src/stasis/anastasis_db_plugin.c @@ -0,0 +1,146 @@ +/* + This file is part of TALER + Copyright (C) 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 Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file merchantdb/merchantdb_plugin.c + * @brief Logic to load database plugin + * @author Christian Grothoff + * @author Sree Harsha Totakura + */ +#include "platform.h" +#include "anastasis_database_plugin.h" +#include + + +/** + * Initialize the plugin. + * + * @param cfg configuration to use + * @return #GNUNET_OK on success + */ +struct ANASTASIS_DatabasePlugin * +ANASTASIS_DB_plugin_load (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + char *plugin_name; + char *lib_name; + struct ANASTASIS_DatabasePlugin *plugin; + + if (GNUNET_SYSERR == + GNUNET_CONFIGURATION_get_value_string (cfg, + "anastasis", + "db", + &plugin_name)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "db"); + return NULL; + } + (void) GNUNET_asprintf (&lib_name, + "libanastasis_plugin_db_%s", + plugin_name); + GNUNET_free (plugin_name); + plugin = GNUNET_PLUGIN_load (lib_name, + (void *) cfg); + if (NULL != plugin) + plugin->library_name = lib_name; + else + lib_name = NULL; + return plugin; +} + + +/** + * Shutdown the plugin. + * + * @param plugin the plugin to unload + */ +void +ANASTASIS_DB_plugin_unload (struct ANASTASIS_DatabasePlugin *plugin) +{ + char *lib_name; + + if (NULL == plugin) + return; + lib_name = plugin->library_name; + GNUNET_assert (NULL == GNUNET_PLUGIN_unload (lib_name, + plugin)); + GNUNET_free (lib_name); +} + + +/** + * Libtool search path before we started. + */ +static char *old_dlsearchpath; + + +/** + * Setup libtool paths. + */ +void __attribute__ ((constructor)) +plugin_init () +{ + 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)) +plugin_fini () +{ + lt_dlsetsearchpath (old_dlsearchpath); + if (NULL != old_dlsearchpath) + { + GNUNET_free (old_dlsearchpath); + } + lt_dlexit (); +} + + +/* end of anastasis_db_plugin.c */ diff --git a/src/stasis/anastasis_db_postgres.conf b/src/stasis/anastasis_db_postgres.conf new file mode 100644 index 0000000..71a21ac --- /dev/null +++ b/src/stasis/anastasis_db_postgres.conf @@ -0,0 +1,7 @@ +[anastasis] +#The DB plugin to use +DB = postgres + +[stasis-postgres] +#The connection string the plugin has to use for connecting to the database +CONFIG = postgres:///anastasis diff --git a/src/stasis/drop0001.sql b/src/stasis/drop0001.sql new file mode 100644 index 0000000..afc457d --- /dev/null +++ b/src/stasis/drop0001.sql @@ -0,0 +1,37 @@ +-- +-- This file is part of ANASTASIS +-- Copyright (C) 2014--2020 Anastasis Systems SA +-- +-- ANASTASIS 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. +-- +-- 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. If not, see +-- + +-- Everything in one big transaction +BEGIN; + +-- This script DROPs all of the tables we create. +-- +-- Unlike the other SQL files, it SHOULD be updated to reflect the +-- latest requirements for dropping tables. + +-- Drops for 0001.sql +DROP TABLE IF EXISTS anastasis_truth CASCADE; +DROP TABLE IF EXISTS anastasis_user CASCADE; +DROP TABLE IF EXISTS anastasis_recdoc_payment; +DROP TABLE IF EXISTS anastasis_recoverydocument; +DROP TABLE IF EXISTS anastasis_challengecode; +DROP TABLE IF EXISTS anastasis_challenge_payment; + +-- Unregister patch (0001.sql) +SELECT _v.unregister_patch('stasis-0001'); + +-- And we're out of here... +COMMIT; diff --git a/src/stasis/plugin_anastasis_postgres.c b/src/stasis/plugin_anastasis_postgres.c new file mode 100644 index 0000000..4aba97c --- /dev/null +++ b/src/stasis/plugin_anastasis_postgres.c @@ -0,0 +1,2301 @@ +/* + 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 +*/ +/** + * @file anastasis/plugin_anastasisdb_postgres.c + * @brief database helper functions for postgres used by the anastasis + * @author Sree Harsha Totakura + * @author Christian Grothoff + * @author Marcello Stanisci + */ +#include "platform.h" +#include "anastasis_database_plugin.h" +#include "anastasis_database_lib.h" +#include + +/** + * How long do we keep transient accounts open (those that have + * not been paid at all, but are awaiting payment). This puts + * a cap on how long users have to make a payment after a payment + * request was generated. + */ +#define TRANSIENT_LIFETIME GNUNET_TIME_UNIT_WEEKS + +/** + * How often do we re-try if we run into a DB serialization error? + */ +#define MAX_RETRIES 3 + + +/** + * Type of the "cls" argument given to each of the functions in + * our API. + */ +struct PostgresClosure +{ + + /** + * Postgres connection handle. + */ + struct GNUNET_PQ_Context *conn; + + /** + * Underlying configuration. + */ + const struct GNUNET_CONFIGURATION_Handle *cfg; + + /** + * Name of the currently active transaction, NULL if none is active. + */ + const char *transaction_name; + + /** + * Currency we accept payments in. + */ + char *currency; + +}; + + +/** + * Drop anastasis tables + * + * @param cls closure our `struct Plugin` + * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure + */ +static int +postgres_drop_tables (void *cls) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_Context *conn; + + conn = GNUNET_PQ_connect_with_cfg (pg->cfg, + "stasis-postgres", + "drop", + NULL, + NULL); + if (NULL == conn) + return GNUNET_SYSERR; + GNUNET_PQ_disconnect (conn); + return GNUNET_OK; +} + + +/** + * Check that the database connection is still up. + * + * @param pg connection to check + */ +static void +check_connection (void *cls) +{ + struct PostgresClosure *pg = cls; + + GNUNET_PQ_reconnect_if_down (pg->conn); +} + + +/** + * 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 + */ +static void +postgres_preflight (void *cls) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_ExecuteStatement es[] = { + GNUNET_PQ_make_execute ("COMMIT"), + GNUNET_PQ_EXECUTE_STATEMENT_END + }; + + if (NULL == pg->transaction_name) + return; /* all good */ + if (GNUNET_OK == + GNUNET_PQ_exec_statements (pg->conn, + es)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "BUG: Preflight check committed transaction `%s'!\n", + pg->transaction_name); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "BUG: Preflight check failed to commit transaction `%s'!\n", + pg->transaction_name); + } + pg->transaction_name = NULL; +} + + +/** + * 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 + */ +static int +begin_transaction (void *cls, + const char *name) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_ExecuteStatement es[] = { + GNUNET_PQ_make_execute ("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"), + GNUNET_PQ_EXECUTE_STATEMENT_END + }; + + check_connection (pg); + postgres_preflight (pg); + pg->transaction_name = name; + if (GNUNET_OK != + GNUNET_PQ_exec_statements (pg->conn, + es)) + { + TALER_LOG_ERROR ("Failed to start transaction\n"); + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** +* Roll back the current transaction of a database connection. +* +* @param cls the `struct PostgresClosure` with the plugin-specific state +* @return #GNUNET_OK on success +*/ +static void +rollback (void *cls) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_ExecuteStatement es[] = { + GNUNET_PQ_make_execute ("ROLLBACK"), + GNUNET_PQ_EXECUTE_STATEMENT_END + }; + + if (GNUNET_OK != + GNUNET_PQ_exec_statements (pg->conn, + es)) + { + TALER_LOG_ERROR ("Failed to rollback transaction\n"); + GNUNET_break (0); + } + pg->transaction_name = NULL; +} + + +/** + * Commit the current transaction of a database connection. + * + * @param cls the `struct PostgresClosure` with the plugin-specific state + * @return transaction status code + */ +static enum GNUNET_DB_QueryStatus +commit_transaction (void *cls) +{ + struct PostgresClosure *pg = cls; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_PQ_QueryParam no_params[] = { + GNUNET_PQ_query_param_end + }; + + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "do_commit", + no_params); + pg->transaction_name = NULL; + return qs; +} + + +/** + * 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 + */ +static enum GNUNET_DB_QueryStatus +postgres_gc (void *cls, + struct GNUNET_TIME_Absolute expire_backups, + struct GNUNET_TIME_Absolute expire_pending_payments) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_absolute_time (&expire_backups), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_QueryParam params2[] = { + GNUNET_PQ_query_param_absolute_time (&expire_pending_payments), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + postgres_preflight (pg); + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "gc_accounts", + params); + if (qs < 0) + return qs; + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "gc_recdoc_pending_payments", + params2); +} + + +/** + * 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, ... + */ +static enum ANASTASIS_DB_StoreStatus +postgres_store_recovery_document ( + void *cls, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + const struct ANASTASIS_AccountSignatureP *account_sig, + const struct GNUNET_HashCode *recovery_data_hash, + const void *recovery_data, + size_t recovery_data_size, + const struct ANASTASIS_PaymentSecretP *payment_secret, + uint32_t *version) +{ + struct PostgresClosure *pg = cls; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + postgres_preflight (pg); + for (unsigned int retry = 0; retryconn, + "latest_recovery_version_select", + params, + rs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + *version = 1; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* had an existing recovery_data, is it identical? */ + if (0 == GNUNET_memcmp (&dh, + recovery_data_hash)) + { + /* Yes. Previous identical recovery data exists */ + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_NO_RESULTS; + } + (*version)++; + break; + default: + rollback (pg); + return qs; + } + } + + /* First, check if account exists */ + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_end + }; + + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "user_select", + params, + rs); + } + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_PAYMENT_REQUIRED; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* handle interesting case below */ + break; + } + + { + uint32_t postcounter; + + /* lookup if the user has enough uploads left and decrement */ + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_auto_from_type (payment_secret), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint32 ("post_counter", + &postcounter), + GNUNET_PQ_result_spec_end + }; + + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "postcounter_select", + params, + rs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + if (0 == postcounter) + { + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_STORE_LIMIT_EXCEEDED; + } + /* Decrement the postcounter by one */ + postcounter--; + + /* Update the postcounter in the Database */ + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint32 (&postcounter), + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_auto_from_type (payment_secret), + GNUNET_PQ_query_param_end + }; + + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "postcounter_update", + params); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + default: + rollback (pg); + return qs; + } + } + } + + /* finally, actually insert the recovery document */ + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_uint32 (version), + GNUNET_PQ_query_param_auto_from_type (account_sig), + GNUNET_PQ_query_param_auto_from_type (recovery_data_hash), + GNUNET_PQ_query_param_fixed_size (recovery_data, + recovery_data_size), + GNUNET_PQ_query_param_end + }; + + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "recovery_document_insert", + params); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + rollback (pg); + return ANASTASIS_DB_STORE_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + qs = commit_transaction (pg); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + goto retry; + if (qs < 0) + return ANASTASIS_DB_STORE_STATUS_HARD_ERROR; + return ANASTASIS_DB_STORE_STATUS_SUCCESS; + } + } +retry: + rollback (pg); + } + return ANASTASIS_DB_STORE_STATUS_SOFT_ERROR; +} + + +/** + * Increment account lifetime. + * + * @param cls closure + * @param anastasis_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 + */ +static enum GNUNET_DB_QueryStatus +postgres_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) +{ + struct PostgresClosure *pg = cls; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + for (unsigned int retries = 0; retriesconn, + "recdoc_payment_done", + params); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + *paid_until = GNUNET_TIME_UNIT_ZERO_ABS; + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* continued below */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* continued below */ + break; + } + } + + { + enum GNUNET_DB_QueryStatus qs2; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_end + }; + struct GNUNET_TIME_Absolute expiration; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_absolute_time ("expiration_date", + &expiration), + GNUNET_PQ_result_spec_end + }; + + qs2 = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "user_select", + params, + rs); + switch (qs2) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + return qs2; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* inconsistent, cannot have recdoc payment but no user!? */ + GNUNET_break (0); + rollback (pg); + return GNUNET_DB_STATUS_HARD_ERROR; + } + else + { + /* user does not exist, create new one */ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_absolute_time (&expiration), + GNUNET_PQ_query_param_end + }; + + expiration = GNUNET_TIME_relative_to_absolute (lifetime); + GNUNET_break (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us != + expiration.abs_value_us); + *paid_until = expiration; + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "user_insert", + params); + } + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* existing rec doc payment, return expiration */ + *paid_until = expiration; + rollback (pg); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment existed, lifetime of account %s unchanged at %s\n", + TALER_B2S (anastasis_pub), + GNUNET_STRINGS_absolute_time_to_string (*paid_until)); + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + else + { + /* user exists, update expiration_date */ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_absolute_time (&expiration), + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_end + }; + + expiration = GNUNET_TIME_absolute_add (expiration, + lifetime); + GNUNET_break (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us != + expiration.abs_value_us); + *paid_until = expiration; + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "user_update", + params); + } + break; + } + } + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + rollback (pg); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + qs = commit_transaction (pg); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + goto retry; + if (qs < 0) + return GNUNET_DB_STATUS_HARD_ERROR; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Incremented lifetime of account %s to %s\n", + TALER_B2S (anastasis_pub), + GNUNET_STRINGS_absolute_time_to_string (*paid_until)); + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +retry: + rollback (pg); + } + return GNUNET_DB_STATUS_SOFT_ERROR; +} + + +/** + * 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 + */ +static enum GNUNET_DB_QueryStatus +postgres_update_lifetime ( + void *cls, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub, + const struct ANASTASIS_PaymentSecretP *payment_identifier, + struct GNUNET_TIME_Absolute eol) +{ + struct PostgresClosure *pg = cls; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + for (unsigned int retries = 0; retriesconn, + "recdoc_payment_done", + params); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + goto retry; + if (0 >= qs) + { + /* same payment made before, or unknown, or error + => no further action! */ + rollback (pg); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment existed, lifetime of account %s unchanged\n", + TALER_B2S (anastasis_pub)); + return qs; + } + } + + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_end + }; + struct GNUNET_TIME_Absolute expiration; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_absolute_time ("expiration_date", + &expiration), + GNUNET_PQ_result_spec_end + }; + + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "user_select", + params, + rs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + { + /* user does not exist, create new one */ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_absolute_time (&eol), + GNUNET_PQ_query_param_end + }; + + GNUNET_break (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us != + eol.abs_value_us); + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "user_insert", + params); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Created new account %s with expiration %s\n", + TALER_B2S (anastasis_pub), + GNUNET_STRINGS_absolute_time_to_string (eol)); + } + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + { + /* user exists, update expiration_date */ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_absolute_time (&expiration), + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_end + }; + + expiration = GNUNET_TIME_absolute_max (expiration, + eol); + GNUNET_break (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us != + expiration.abs_value_us); + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "user_update", + params); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Updated account %s to new expiration %s\n", + TALER_B2S (anastasis_pub), + GNUNET_STRINGS_absolute_time_to_string (expiration)); + } + break; + } + } + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + rollback (pg); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + qs = commit_transaction (pg); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + goto retry; + if (qs < 0) + return GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +retry: + rollback (pg); + } + return GNUNET_DB_STATUS_SOFT_ERROR; +} + + +/** + * 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 + */ +static enum GNUNET_DB_QueryStatus +postgres_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) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + struct GNUNET_TIME_Absolute expiration; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_uint32 (&post_counter), + TALER_PQ_query_param_amount (amount), + GNUNET_PQ_query_param_auto_from_type (payment_secret), + GNUNET_PQ_query_param_absolute_time (&now), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + postgres_preflight (pg); + + /* because of constraint at user_id, first we have to verify + if user exists, and if not, create one */ + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_absolute_time ("expiration_date", + &expiration), + GNUNET_PQ_result_spec_end + }; + + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "user_select", + params, + rs); + } + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + { + /* create new user with short lifetime */ + struct GNUNET_TIME_Absolute exp + = GNUNET_TIME_relative_to_absolute (TRANSIENT_LIFETIME); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_absolute_time (&exp), + GNUNET_PQ_query_param_end + }; + + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "user_insert", + params); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* successful, continue below */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Created new account %s with transient life until %s\n", + TALER_B2S (anastasis_pub), + GNUNET_STRINGS_absolute_time_to_string (exp)); + break; + } + } + /* continue below */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* handle case below */ + break; + } + + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "recdoc_payment_insert", + params); +} + + +/** + * 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 + */ +static enum GNUNET_DB_QueryStatus +postgres_record_truth_upload_payment ( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid, + const struct TALER_Amount *amount, + struct GNUNET_TIME_Relative duration) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_TIME_Absolute exp = GNUNET_TIME_relative_to_absolute (duration); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (uuid), + TALER_PQ_query_param_amount (amount), + GNUNET_PQ_query_param_absolute_time (&exp), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "truth_payment_insert", + params); +} + + +/** + * 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 + */ +static enum GNUNET_DB_QueryStatus +postgres_check_truth_upload_paid ( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid, + struct GNUNET_TIME_Absolute *paid_until) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (uuid), + GNUNET_PQ_query_param_absolute_time (&now), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_absolute_time ("expiration", + paid_until), + GNUNET_PQ_result_spec_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "truth_payment_select", + params, + rs); +} + + +/** + * 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 + */ +static enum GNUNET_DB_QueryStatus +postgres_record_challenge_payment ( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const struct ANASTASIS_PaymentSecretP *payment_secret, + const struct TALER_Amount *amount) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + TALER_PQ_query_param_amount (amount), + GNUNET_PQ_query_param_auto_from_type (payment_secret), + GNUNET_PQ_query_param_absolute_time (&now), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "challenge_payment_insert", + params); +} + + +/** + * Store refund granted 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 + */ +static enum GNUNET_DB_QueryStatus +postgres_record_challenge_refund ( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const struct ANASTASIS_PaymentSecretP *payment_secret) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (payment_secret), + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "challenge_refund_update", + params); +} + + +/** + * 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 + */ +static enum GNUNET_DB_QueryStatus +postgres_check_challenge_payment ( + void *cls, + const struct ANASTASIS_PaymentSecretP *payment_secret, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + bool *paid) +{ + struct PostgresClosure *pg = cls; + uint8_t paid8; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (payment_secret), + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("paid", + &paid8), + GNUNET_PQ_result_spec_end + }; + + check_connection (pg); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "challenge_payment_select", + params, + rs); + *paid = (0 != paid8); + return qs; +} + + +/** + * 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 + */ +static enum GNUNET_DB_QueryStatus +postgres_check_payment_identifier ( + void *cls, + const struct ANASTASIS_PaymentSecretP *payment_secret, + bool *paid, + bool *valid_counter) +{ + struct PostgresClosure *pg = cls; + uint32_t counter; + uint8_t paid8; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (payment_secret), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("paid", + &paid8), + GNUNET_PQ_result_spec_uint32 ("post_counter", + &counter), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "recdoc_payment_select", + params, + rs); + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + if (counter > 0) + *valid_counter = true; + else + *valid_counter = false; + *paid = (0 != paid8); + } + return qs; +} + + +/** + * 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 + */ +static enum GNUNET_DB_QueryStatus +postgres_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) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_TIME_Absolute expiration = GNUNET_TIME_absolute_get (); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + GNUNET_PQ_query_param_auto_from_type (key_share_data), + GNUNET_PQ_query_param_string (method), + GNUNET_PQ_query_param_fixed_size (encrypted_truth, + encrypted_truth_size), + GNUNET_PQ_query_param_string (mime_type), + TALER_PQ_query_param_absolute_time (&expiration), + GNUNET_PQ_query_param_end + }; + + + expiration = GNUNET_TIME_absolute_add (expiration, + truth_expiration); + GNUNET_TIME_round_abs (&expiration); + check_connection (pg); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "truth_insert", + params); +} + + +/** + * 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 +postgres_get_escrow_challenge ( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + void **truth, + size_t *truth_size, + char **truth_mime, + char **method) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_variable_size ("encrypted_truth", + truth, + truth_size), + GNUNET_PQ_result_spec_string ("truth_mime", + truth_mime), + GNUNET_PQ_result_spec_string ("method_name", + method), + GNUNET_PQ_result_spec_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "truth_select", + params, + rs); +} + + +/** + * Lookup (encrypted) key share by @a truth_uuid. + * + * @param cls closure + * @param truth_uuid the identifier for the Truth + * @param[out] key_share contains the encrypted Keyshare + * @return transaction status + */ +enum GNUNET_DB_QueryStatus +postgres_get_key_share ( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct ANASTASIS_CRYPTO_EncryptedKeyShareP *key_share) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("key_share_data", + key_share), + GNUNET_PQ_result_spec_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "key_share_select", + params, + rs); +} + + +/** + * 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 +postgres_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) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + postgres_preflight (pg); + { + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_absolute_time ("expiration_date", + paid_until), + GNUNET_PQ_result_spec_auto_from_type ("recovery_data_hash", + recovery_data_hash), + GNUNET_PQ_result_spec_uint32 ("version", + version), + GNUNET_PQ_result_spec_end + }; + + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "latest_recovery_version_select", + params, + rs); + } + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + break; /* handle interesting case below */ + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED; + } + + /* check if account exists */ + { + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_absolute_time ("expiration_date", + paid_until), + GNUNET_PQ_result_spec_end + }; + + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "user_select", + params, + rs); + } + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* indicates: no account */ + return ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* indicates: no backup */ + *version = UINT32_MAX; + memset (recovery_data_hash, + 0, + sizeof (*recovery_data_hash)); + return ANASTASIS_DB_ACCOUNT_STATUS_NO_RESULTS; + default: + GNUNET_break (0); + return ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR; + } +} + + +/** + * 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 data_size size of data blob + * @param data blob which contains the recovery document + * @param version[OUT] set to the version number of the policy being returned + * @return transaction status + */ +enum GNUNET_DB_QueryStatus +postgres_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) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint32 ("version", + version), + GNUNET_PQ_result_spec_auto_from_type ("account_sig", + account_sig), + GNUNET_PQ_result_spec_auto_from_type ("recovery_data_hash", + recovery_data_hash), + GNUNET_PQ_result_spec_variable_size ("recovery_data", + data, + data_size), + GNUNET_PQ_result_spec_end + }; + + check_connection (pg); + postgres_preflight (pg); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "latest_recoverydocument_select", + params, + rs); +} + + +/** + * 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 +postgres_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) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (anastasis_pub), + GNUNET_PQ_query_param_uint32 (&version), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("account_sig", + account_sig), + GNUNET_PQ_result_spec_auto_from_type ("recovery_data_hash", + recovery_data_hash), + GNUNET_PQ_result_spec_variable_size ("recovery_data", + data, + data_size), + GNUNET_PQ_result_spec_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "recoverydocument_select", + params, + rs); +} + + +/** + * Closure for check_valid_code(). + */ +struct CheckValidityContext +{ + /** + * Code to check for. + */ + const struct GNUNET_HashCode *hashed_code; + + /** + * Truth we are processing. + */ + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid; + + /** + * Database context. + */ + struct PostgresClosure *pg; + + /** + * Set to true if a code matching @e hashed_code was found. + */ + bool valid; + + /** + * Set to true if we had a database failure. + */ + bool db_failure; + +}; + + +/** + * Helper function for #postgres_verify_challenge_code(). + * To be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls closure of type `struct CheckValidityContext *` + * @param result the postgres result + * @param num_result the number of results in @a result + */ +static void +check_valid_code (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct CheckValidityContext *cvc = cls; + struct PostgresClosure *pg = cvc->pg; + + for (unsigned int i = 0; i < num_results; i++) + { + uint64_t server_code; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("code", + &server_code), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + cvc->db_failure = true; + return; + } + { + struct GNUNET_HashCode shashed_code; + + ANASTASIS_hash_answer (server_code, + &shashed_code); + if (0 == + GNUNET_memcmp (&shashed_code, + cvc->hashed_code)) + { + cvc->valid = true; + } + else + { + /* count failures to prevent brute-force attacks */ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (cvc->truth_uuid), + GNUNET_PQ_query_param_uint64 (&server_code), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "challengecode_update_retry", + params); + if (qs <= 0) + { + GNUNET_break (0); + cvc->db_failure = true; + } + } + } + } +} + + +/** + * 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 code validity status + */ +enum ANASTASIS_DB_CodeStatus +postgres_verify_challenge_code ( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const struct GNUNET_HashCode *hashed_code) +{ + struct PostgresClosure *pg = cls; + struct CheckValidityContext cvc = { + .truth_uuid = truth_uuid, + .hashed_code = hashed_code, + .pg = pg + }; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + TALER_PQ_query_param_absolute_time (&now), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + GNUNET_TIME_round_abs (&now); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "challengecode_select", + params, + &check_valid_code, + &cvc); + if ( (qs < 0) || + (cvc.db_failure) ) + return ANASTASIS_DB_CODE_STATUS_HARD_ERROR; + if (cvc.valid) + return ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED; + if (0 == qs) + return ANASTASIS_DB_CODE_STATUS_NO_RESULTS; + return ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH; +} + + +/** + * Lookup 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 +postgres_lookup_challenge_payment ( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct ANASTASIS_PaymentSecretP *payment_secret) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + struct GNUNET_TIME_Absolute recent + = GNUNET_TIME_absolute_subtract (now, + ANASTASIS_CHALLENGE_OFFER_LIFETIME); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + GNUNET_PQ_query_param_absolute_time (&recent), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("payment_identifier", + payment_secret), + GNUNET_PQ_result_spec_end + }; + + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "challenge_pending_payment_select", + params, + rs); +} + + +/** + * 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 +postgres_update_challenge_payment ( + void *cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const struct ANASTASIS_PaymentSecretP *payment_identifier) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (payment_identifier), + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "challenge_payment_done", + params); +} + + +/** + * Create 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 +postgres_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) +{ + struct PostgresClosure *pg = cls; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + struct GNUNET_TIME_Absolute expiration_date; + struct GNUNET_TIME_Absolute ex_rot; + + check_connection (pg); + GNUNET_TIME_round_abs (&now); + expiration_date = GNUNET_TIME_absolute_add (now, + validity_period); + ex_rot = GNUNET_TIME_absolute_subtract (now, + rotation_period); + for (unsigned int retries = 0; retriesconn, + "challengecode_select_meta", + params, + rs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + rollback (pg); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* no active challenge, create fresh one (below) */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (0 == old_retry_counter) + { + rollback (pg); + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + rollback (pg); + return qs; + } + } + + *code = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE, + INT64_MAX); + *retransmission_date = GNUNET_TIME_UNIT_ZERO_ABS; + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + GNUNET_PQ_query_param_uint64 (code), + TALER_PQ_query_param_absolute_time (&now), + TALER_PQ_query_param_absolute_time (&expiration_date), + GNUNET_PQ_query_param_uint32 (&retry_counter), + GNUNET_PQ_query_param_end + }; + + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "challengecode_insert", + params); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + rollback (pg); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + rollback (pg); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + qs = commit_transaction (pg); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + goto retry; + if (qs < 0) + return qs; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +retry: + rollback (pg); + } + return GNUNET_DB_STATUS_SOFT_ERROR; +} + + +/** + * Remember in the database that we successfully sent a challenge. + * + * @param cls closure + * @param payment_secret payment secret which the user must provide with every upload + * @param truth_uuid the identifier for the challenge + * @param code the challenge that was sent + */ +static enum GNUNET_DB_QueryStatus +postgres_mark_challenge_sent ( + void *cls, + const struct ANASTASIS_PaymentSecretP *payment_secret, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code) +{ + struct PostgresClosure *pg = cls; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + { + struct GNUNET_TIME_Absolute now; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + GNUNET_PQ_query_param_uint64 (&code), + TALER_PQ_query_param_absolute_time (&now), + GNUNET_PQ_query_param_end + }; + + now = GNUNET_TIME_absolute_get (); + GNUNET_TIME_round_abs (&now); + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "challengecode_mark_sent", + params); + if (qs <= 0) + return qs; + } + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (truth_uuid), + GNUNET_PQ_query_param_auto_from_type (payment_secret), + GNUNET_PQ_query_param_end + }; + + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "challengepayment_dec_counter", + params); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* probably was free */ + return qs; + } +} + + +/** + * Function called to remove all expired codes from the database. + * FIXME maybe implemented as part of postgres_gc() in the future. + * + * @return transaction status + */ +enum GNUNET_DB_QueryStatus +postgres_challenge_gc (void *cls) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_TIME_Absolute time_now = GNUNET_TIME_absolute_get (); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_absolute_time (&time_now), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + postgres_preflight (pg); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "gc_challengecodes", + params); +} + + +/** + * Initialize Postgres database subsystem. + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct TALER_ANASTASISDB_Plugin` + */ +void * +libanastasis_plugin_db_postgres_init (void *cls) +{ + struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct PostgresClosure *pg; + struct ANASTASIS_DatabasePlugin *plugin; + struct GNUNET_PQ_PreparedStatement ps[] = { + GNUNET_PQ_make_prepare ("user_insert", + "INSERT INTO anastasis_user " + "(user_id" + ",expiration_date" + ") VALUES " + "($1, $2);", + 2), + GNUNET_PQ_make_prepare ("do_commit", + "COMMIT", + 0), + GNUNET_PQ_make_prepare ("user_select", + "SELECT" + " expiration_date " + "FROM anastasis_user" + " WHERE user_id=$1" + " FOR UPDATE;", + 1), + GNUNET_PQ_make_prepare ("user_update", + "UPDATE anastasis_user" + " SET " + " expiration_date=$1" + " WHERE user_id=$2;", + 2), + GNUNET_PQ_make_prepare ("recdoc_payment_insert", + "INSERT INTO anastasis_recdoc_payment " + "(user_id" + ",post_counter" + ",amount_val" + ",amount_frac" + ",payment_identifier" + ",creation_date" + ") VALUES " + "($1, $2, $3, $4, $5, $6);", + 6), + GNUNET_PQ_make_prepare ("challenge_payment_insert", + "INSERT INTO anastasis_challenge_payment " + "(truth_uuid" + ",amount_val" + ",amount_frac" + ",payment_identifier" + ",creation_date" + ") VALUES " + "($1, $2, $3, $4, $5);", + 5), + GNUNET_PQ_make_prepare ("truth_payment_insert", + "INSERT INTO anastasis_truth_payment " + "(truth_uuid" + ",amount_val" + ",amount_frac" + ",expiration" + ") VALUES " + "($1, $2, $3, $4);", + 4), + GNUNET_PQ_make_prepare ("recdoc_payment_done", + "UPDATE anastasis_recdoc_payment " + "SET" + " paid=TRUE " + "WHERE" + " payment_identifier=$1" + " AND" + " user_id=$2" + " AND" + " paid=FALSE;", + 2), + GNUNET_PQ_make_prepare ("challenge_refund_update", + "UPDATE anastasis_challenge_payment " + "SET" + " refunded=TRUE " + "WHERE" + " payment_identifier=$1" + " AND" + " paid=TRUE" + " AND" + " truth_uuid=$2;", + 2), + GNUNET_PQ_make_prepare ("challenge_payment_done", + "UPDATE anastasis_challenge_payment " + "SET" + " paid=TRUE " + "WHERE" + " payment_identifier=$1" + " AND" + " refunded=FALSE" + " AND" + " truth_uuid=$2" + " AND" + " paid=FALSE;", + 2), + GNUNET_PQ_make_prepare ("recdoc_payment_select", + "SELECT" + " creation_date" + ",post_counter" + ",amount_val" + ",amount_frac" + ",paid" + " FROM anastasis_recdoc_payment" + " WHERE payment_identifier=$1;", + 1), + GNUNET_PQ_make_prepare ("truth_payment_select", + "SELECT" + " expiration" + " FROM anastasis_truth_payment" + " WHERE truth_uuid=$1" + " AND expiration>$2;", + 2), + GNUNET_PQ_make_prepare ("challenge_payment_select", + "SELECT" + " creation_date" + ",amount_val" + ",amount_frac" + ",paid" + " FROM anastasis_challenge_payment" + " WHERE payment_identifier=$1" + " AND truth_uuid=$2" + " AND refunded=FALSE" + " AND counter>0;", + 1), + GNUNET_PQ_make_prepare ("challenge_pending_payment_select", + "SELECT" + " creation_date" + ",payment_identifier" + ",amount_val" + ",amount_frac" + " FROM anastasis_challenge_payment" + " WHERE" + " paid=FALSE" + " AND" + " refunded=FALSE" + " AND" + " truth_uuid=$1" + " AND" + " creation_date > $2;", + 1), + GNUNET_PQ_make_prepare ("recdoc_payments_select", + "SELECT" + " user_id" + ",payment_identifier" + ",amount_val" + ",amount_frac" + " FROM anastasis_recdoc_payment" + " WHERE paid=FALSE;", + 0), + GNUNET_PQ_make_prepare ("gc_accounts", + "DELETE FROM anastasis_user " + "WHERE" + " expiration_date < $1;", + 1), + GNUNET_PQ_make_prepare ("gc_recdoc_pending_payments", + "DELETE FROM anastasis_recdoc_payment " + "WHERE" + " paid=FALSE" + " AND" + " creation_date < $1;", + 1), + GNUNET_PQ_make_prepare ("gc_challenge_pending_payments", + "DELETE FROM anastasis_challenge_payment " + "WHERE" + " (paid=FALSE" + " OR" + " refunded=TRUE)" + " AND" + " creation_date < $1;", + 1), + GNUNET_PQ_make_prepare ("truth_insert", + "INSERT INTO anastasis_truth " + "(truth_uuid" + ",key_share_data" + ",method_name" + ",encrypted_truth" + ",truth_mime" + ",expiration" + ") VALUES " + "($1, $2, $3, $4, $5, $6);", + 6), + GNUNET_PQ_make_prepare ("recovery_document_insert", + "INSERT INTO anastasis_recoverydocument " + "(user_id" + ",version" + ",account_sig" + ",recovery_data_hash" + ",recovery_data" + ") VALUES " + "($1, $2, $3, $4, $5);", + 5), + GNUNET_PQ_make_prepare ("truth_select", + "SELECT " + " method_name" + ",encrypted_truth" + ",truth_mime" + " FROM anastasis_truth" + " WHERE truth_uuid =$1;", + 1), + GNUNET_PQ_make_prepare ("latest_recoverydocument_select", + "SELECT " + " version" + ",account_sig" + ",recovery_data_hash" + ",recovery_data" + " FROM anastasis_recoverydocument" + " WHERE user_id =$1 " + " ORDER BY version DESC" + " LIMIT 1;", + 1), + GNUNET_PQ_make_prepare ("latest_recovery_version_select", + "SELECT" + " version" + ",recovery_data_hash" + ",expiration_date" + " FROM anastasis_recoverydocument" + " JOIN anastasis_user USING (user_id)" + " WHERE user_id=$1" + " ORDER BY version DESC" + " LIMIT 1;", + 1), + GNUNET_PQ_make_prepare ("recoverydocument_select", + "SELECT " + " account_sig" + ",recovery_data_hash" + ",recovery_data" + " FROM anastasis_recoverydocument" + " WHERE user_id=$1" + " AND version=$2;", + 2), + GNUNET_PQ_make_prepare ("postcounter_select", + "SELECT" + " post_counter" + " FROM anastasis_recdoc_payment" + " WHERE user_id=$1" + " AND payment_identifier=$2;", + 2), + GNUNET_PQ_make_prepare ("postcounter_update", + "UPDATE " + "anastasis_recdoc_payment " + "SET " + "post_counter=$1 " + "WHERE user_id =$2 " + "AND payment_identifier=$3;", + 3), + GNUNET_PQ_make_prepare ("key_share_select", + "SELECT " + "key_share_data " + "FROM " + "anastasis_truth " + "WHERE truth_uuid =$1;", + 1), + GNUNET_PQ_make_prepare ("challengecode_insert", + "INSERT INTO anastasis_challengecode " + "(truth_uuid" + ",code" + ",creation_date" + ",expiration_date" + ",retry_counter" + ") VALUES " + "($1, $2, $3, $4, $5);", + 5), + GNUNET_PQ_make_prepare ("challengecode_select", + "SELECT " + " code" + " FROM anastasis_challengecode" + " WHERE truth_uuid=$1" + " AND expiration_date > $2" + " AND retry_counter > 0;", + 2), + GNUNET_PQ_make_prepare ("challengecode_select_meta", + "SELECT " + " code" + ",retry_counter" + ",retransmission_date" + " FROM anastasis_challengecode" + " WHERE truth_uuid=$1" + " AND expiration_date > $2" + " AND creation_date > $3" + " ORDER BY creation_date DESC" + " LIMIT 1;", + 2), + GNUNET_PQ_make_prepare ("challengecode_update_retry", + "UPDATE anastasis_challengecode" + " SET retry_counter=retry_counter - 1" + " WHERE truth_uuid=$1" + " AND code=$2" + " AND retry_counter > 0;", + 1), + GNUNET_PQ_make_prepare ("challengepayment_dec_counter", + "UPDATE anastasis_challenge_payment" + " SET counter=counter - 1" + " WHERE truth_uuid=$1" + " AND payment_identifier=$2" + " AND counter > 0;", + 2), + GNUNET_PQ_make_prepare ("challengecode_mark_sent", + "UPDATE anastasis_challengecode" + " SET retransmission_date=$3" + " WHERE truth_uuid=$1" + " AND code=$2" + " AND creation_date IN" + " (SELECT creation_date" + " FROM anastasis_challengecode" + " WHERE truth_uuid=$1" + " AND code=$2" + " ORDER BY creation_date DESC" + " LIMIT 1);", + 3), + GNUNET_PQ_make_prepare ("gc_challengecodes", + "DELETE FROM anastasis_challengecode " + "WHERE " + "expiration_date < $1;", + 1), + GNUNET_PQ_PREPARED_STATEMENT_END + }; + + pg = GNUNET_new (struct PostgresClosure); + pg->cfg = cfg; + pg->conn = GNUNET_PQ_connect_with_cfg (cfg, + "stasis-postgres", + "stasis-", + NULL, + ps); + if (NULL == pg->conn) + { + GNUNET_free (pg); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "taler", + "CURRENCY", + &pg->currency)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "taler", + "CURRENCY"); + GNUNET_PQ_disconnect (pg->conn); + GNUNET_free (pg); + return NULL; + } + plugin = GNUNET_new (struct ANASTASIS_DatabasePlugin); + plugin->cls = pg; + plugin->drop_tables = &postgres_drop_tables; + plugin->gc = &postgres_gc; + plugin->preflight = &postgres_preflight; + plugin->rollback = &rollback; + plugin->commit = &commit_transaction; + plugin->store_recovery_document = &postgres_store_recovery_document; + plugin->record_recdoc_payment = &postgres_record_recdoc_payment; + plugin->store_truth = &postgres_store_truth; + plugin->get_escrow_challenge = &postgres_get_escrow_challenge; + plugin->get_key_share = &postgres_get_key_share; + plugin->get_latest_recovery_document = &postgres_get_latest_recovery_document; + plugin->get_recovery_document = &postgres_get_recovery_document; + plugin->lookup_account = &postgres_lookup_account; + plugin->check_payment_identifier = &postgres_check_payment_identifier; + plugin->increment_lifetime = &postgres_increment_lifetime; + plugin->update_lifetime = &postgres_update_lifetime; + plugin->start = &begin_transaction; + plugin->check_connection = &check_connection; + plugin->verify_challenge_code = &postgres_verify_challenge_code; + plugin->create_challenge_code = &postgres_create_challenge_code; + plugin->mark_challenge_sent = &postgres_mark_challenge_sent; + plugin->challenge_gc = &postgres_challenge_gc; + plugin->record_truth_upload_payment = &postgres_record_truth_upload_payment; + plugin->check_truth_upload_paid = &postgres_check_truth_upload_paid; + plugin->record_challenge_payment = &postgres_record_challenge_payment; + plugin->record_challenge_refund = &postgres_record_challenge_refund; + plugin->check_challenge_payment = &postgres_check_challenge_payment; + plugin->lookup_challenge_payment = &postgres_lookup_challenge_payment; + plugin->update_challenge_payment = &postgres_update_challenge_payment; + return plugin; +} + + +/** + * Shutdown Postgres database subsystem. + * + * @param cls a `struct ANASTASIS_DB_STATUS_Plugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_db_postgres_done (void *cls) +{ + struct ANASTASIS_DatabasePlugin *plugin = cls; + struct PostgresClosure *pg = plugin->cls; + + GNUNET_PQ_disconnect (pg->conn); + GNUNET_free (pg->currency); + GNUNET_free (pg); + GNUNET_free (plugin); + return NULL; +} + + +/* end of plugin_anastasisdb_postgres.c */ diff --git a/src/stasis/stasis-0000.sql b/src/stasis/stasis-0000.sql new file mode 100644 index 0000000..116f409 --- /dev/null +++ b/src/stasis/stasis-0000.sql @@ -0,0 +1,293 @@ +-- LICENSE AND COPYRIGHT +-- +-- Copyright (C) 2010 Hubert depesz Lubaczewski +-- +-- This program is distributed under the (Revised) BSD License: +-- L +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions +-- are met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- +-- * Neither the name of Hubert depesz Lubaczewski's Organization +-- nor the names of its contributors may be used to endorse or +-- promote products derived from this software without specific +-- prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +-- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +-- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +-- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-- +-- Code origin: https://gitlab.com/depesz/Versioning/blob/master/install.versioning.sql +-- +-- +-- # NAME +-- +-- **Versioning** - simplistic take on tracking and applying changes to databases. +-- +-- # DESCRIPTION +-- +-- This project strives to provide simple way to manage changes to +-- database. +-- +-- Instead of making changes on development server, then finding +-- differences between production and development, deciding which ones +-- should be installed on production, and finding a way to install them - +-- you start with writing diffs themselves! +-- +-- # INSTALLATION +-- +-- To install versioning simply run install.versioning.sql in your database +-- (all of them: production, stage, test, devel, ...). +-- +-- # USAGE +-- +-- In your files with patches to database, put whole logic in single +-- transaction, and use \_v.\* functions - usually \_v.register_patch() at +-- least to make sure everything is OK. +-- +-- For example. Let's assume you have patch files: +-- +-- ## 0001.sql: +-- +-- ``` +-- create table users (id serial primary key, username text); +-- ``` +-- +-- ## 0002.sql: +-- +-- ``` +-- insert into users (username) values ('depesz'); +-- ``` +-- To change it to use versioning you would change the files, to this +-- state: +-- +-- 0000.sql: +-- +-- ``` +-- BEGIN; +-- select _v.register_patch('000-base', NULL, NULL); +-- create table users (id serial primary key, username text); +-- COMMIT; +-- ``` +-- +-- ## 0002.sql: +-- +-- ``` +-- BEGIN; +-- select _v.register_patch('001-users', ARRAY['000-base'], NULL); +-- insert into users (username) values ('depesz'); +-- COMMIT; +-- ``` +-- +-- This will make sure that patch 001-users can only be applied after +-- 000-base. +-- +-- # AVAILABLE FUNCTIONS +-- +-- ## \_v.register_patch( TEXT ) +-- +-- Registers named patch, or dies if it is already registered. +-- +-- Returns integer which is id of patch in \_v.patches table - only if it +-- succeeded. +-- +-- ## \_v.register_patch( TEXT, TEXT[] ) +-- +-- Same as \_v.register_patch( TEXT ), but checks is all given patches (given as +-- array in second argument) are already registered. +-- +-- ## \_v.register_patch( TEXT, TEXT[], TEXT[] ) +-- +-- Same as \_v.register_patch( TEXT, TEXT[] ), but also checks if there are no conflicts with preexisting patches. +-- +-- Third argument is array of names of patches that conflict with current one. So +-- if any of them is installed - register_patch will error out. +-- +-- ## \_v.unregister_patch( TEXT ) +-- +-- Removes information about given patch from the versioning data. +-- +-- It doesn't remove objects that were created by this patch - just removes +-- metainformation. +-- +-- ## \_v.assert_user_is_superuser() +-- +-- Make sure that current patch is being loaded by superuser. +-- +-- If it's not - it will raise exception, and break transaction. +-- +-- ## \_v.assert_user_is_not_superuser() +-- +-- Make sure that current patch is not being loaded by superuser. +-- +-- If it is - it will raise exception, and break transaction. +-- +-- ## \_v.assert_user_is_one_of(TEXT, TEXT, ... ) +-- +-- Make sure that current patch is being loaded by one of listed users. +-- +-- If ```current_user``` is not listed as one of arguments - function will raise +-- exception and break the transaction. + +BEGIN; + +-- This file adds versioning support to database it will be loaded to. +-- It requires that PL/pgSQL is already loaded - will raise exception otherwise. +-- All versioning "stuff" (tables, functions) is in "_v" schema. + +-- All functions are defined as 'RETURNS SETOF INT4' to be able to make them to RETURN literally nothing (0 rows). +-- >> RETURNS VOID<< IS similar, but it still outputs "empty line" in psql when calling. +CREATE SCHEMA IF NOT EXISTS _v; +COMMENT ON SCHEMA _v IS 'Schema for versioning data and functionality.'; + +CREATE TABLE IF NOT EXISTS _v.patches ( + patch_name TEXT PRIMARY KEY, + applied_tsz TIMESTAMPTZ NOT NULL DEFAULT now(), + applied_by TEXT NOT NULL, + requires TEXT[], + conflicts TEXT[] +); +COMMENT ON TABLE _v.patches IS 'Contains information about what patches are currently applied on database.'; +COMMENT ON COLUMN _v.patches.patch_name IS 'Name of patch, has to be unique for every patch.'; +COMMENT ON COLUMN _v.patches.applied_tsz IS 'When the patch was applied.'; +COMMENT ON COLUMN _v.patches.applied_by IS 'Who applied this patch (PostgreSQL username)'; +COMMENT ON COLUMN _v.patches.requires IS 'List of patches that are required for given patch.'; +COMMENT ON COLUMN _v.patches.conflicts IS 'List of patches that conflict with given patch.'; + +CREATE OR REPLACE FUNCTION _v.register_patch( IN in_patch_name TEXT, IN in_requirements TEXT[], in_conflicts TEXT[], OUT versioning INT4 ) RETURNS setof INT4 AS $$ +DECLARE + t_text TEXT; + t_text_a TEXT[]; + i INT4; +BEGIN + -- Thanks to this we know only one patch will be applied at a time + LOCK TABLE _v.patches IN EXCLUSIVE MODE; + + SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name; + IF FOUND THEN + RAISE EXCEPTION 'Patch % is already applied!', in_patch_name; + END IF; + + t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE patch_name = any( in_conflicts ) ); + IF array_upper( t_text_a, 1 ) IS NOT NULL THEN + RAISE EXCEPTION 'Versioning patches conflict. Conflicting patche(s) installed: %.', array_to_string( t_text_a, ', ' ); + END IF; + + IF array_upper( in_requirements, 1 ) IS NOT NULL THEN + t_text_a := '{}'; + FOR i IN array_lower( in_requirements, 1 ) .. array_upper( in_requirements, 1 ) LOOP + SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_requirements[i]; + IF NOT FOUND THEN + t_text_a := t_text_a || in_requirements[i]; + END IF; + END LOOP; + IF array_upper( t_text_a, 1 ) IS NOT NULL THEN + RAISE EXCEPTION 'Missing prerequisite(s): %.', array_to_string( t_text_a, ', ' ); + END IF; + END IF; + + INSERT INTO _v.patches (patch_name, applied_tsz, applied_by, requires, conflicts ) VALUES ( in_patch_name, now(), current_user, coalesce( in_requirements, '{}' ), coalesce( in_conflicts, '{}' ) ); + RETURN; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[], TEXT[] ) IS 'Function to register patches in database. Raises exception if there are conflicts, prerequisites are not installed or the migration has already been installed.'; + +CREATE OR REPLACE FUNCTION _v.register_patch( TEXT, TEXT[] ) RETURNS setof INT4 AS $$ + SELECT _v.register_patch( $1, $2, NULL ); +$$ language sql; +COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[] ) IS 'Wrapper to allow registration of patches without conflicts.'; +CREATE OR REPLACE FUNCTION _v.register_patch( TEXT ) RETURNS setof INT4 AS $$ + SELECT _v.register_patch( $1, NULL, NULL ); +$$ language sql; +COMMENT ON FUNCTION _v.register_patch( TEXT ) IS 'Wrapper to allow registration of patches without requirements and conflicts.'; + +CREATE OR REPLACE FUNCTION _v.unregister_patch( IN in_patch_name TEXT, OUT versioning INT4 ) RETURNS setof INT4 AS $$ +DECLARE + i INT4; + t_text_a TEXT[]; +BEGIN + -- Thanks to this we know only one patch will be applied at a time + LOCK TABLE _v.patches IN EXCLUSIVE MODE; + + t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE in_patch_name = ANY( requires ) ); + IF array_upper( t_text_a, 1 ) IS NOT NULL THEN + RAISE EXCEPTION 'Cannot uninstall %, as it is required by: %.', in_patch_name, array_to_string( t_text_a, ', ' ); + END IF; + + DELETE FROM _v.patches WHERE patch_name = in_patch_name; + GET DIAGNOSTICS i = ROW_COUNT; + IF i < 1 THEN + RAISE EXCEPTION 'Patch % is not installed, so it can''t be uninstalled!', in_patch_name; + END IF; + + RETURN; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.unregister_patch( TEXT ) IS 'Function to unregister patches in database. Dies if the patch is not registered, or if unregistering it would break dependencies.'; + +CREATE OR REPLACE FUNCTION _v.assert_patch_is_applied( IN in_patch_name TEXT ) RETURNS TEXT as $$ +DECLARE + t_text TEXT; +BEGIN + SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name; + IF NOT FOUND THEN + RAISE EXCEPTION 'Patch % is not applied!', in_patch_name; + END IF; + RETURN format('Patch %s is applied.', in_patch_name); +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.assert_patch_is_applied( TEXT ) IS 'Function that can be used to make sure that patch has been applied.'; + +CREATE OR REPLACE FUNCTION _v.assert_user_is_superuser() RETURNS TEXT as $$ +DECLARE + v_super bool; +BEGIN + SELECT usesuper INTO v_super FROM pg_user WHERE usename = current_user; + IF v_super THEN + RETURN 'assert_user_is_superuser: OK'; + END IF; + RAISE EXCEPTION 'Current user is not superuser - cannot continue.'; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.assert_user_is_superuser() IS 'Function that can be used to make sure that patch is being applied using superuser account.'; + +CREATE OR REPLACE FUNCTION _v.assert_user_is_not_superuser() RETURNS TEXT as $$ +DECLARE + v_super bool; +BEGIN + SELECT usesuper INTO v_super FROM pg_user WHERE usename = current_user; + IF v_super THEN + RAISE EXCEPTION 'Current user is superuser - cannot continue.'; + END IF; + RETURN 'assert_user_is_not_superuser: OK'; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.assert_user_is_not_superuser() IS 'Function that can be used to make sure that patch is being applied using normal (not superuser) account.'; + +CREATE OR REPLACE FUNCTION _v.assert_user_is_one_of(VARIADIC p_acceptable_users TEXT[] ) RETURNS TEXT as $$ +DECLARE +BEGIN + IF current_user = any( p_acceptable_users ) THEN + RETURN 'assert_user_is_one_of: OK'; + END IF; + RAISE EXCEPTION 'User is not one of: % - cannot continue.', p_acceptable_users; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.assert_user_is_one_of(TEXT[]) IS 'Function that can be used to make sure that patch is being applied by one of defined users.'; + +COMMIT; diff --git a/src/stasis/stasis-0001.sql b/src/stasis/stasis-0001.sql new file mode 100644 index 0000000..beb886d --- /dev/null +++ b/src/stasis/stasis-0001.sql @@ -0,0 +1,194 @@ +-- +-- This file is part of Anastasis +-- Copyright (C) 2020, 2021 Anastasis SARL SA +-- +-- ANASTASIS 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. +-- +-- 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. If not, see +-- + +-- Everything in one big transaction +BEGIN; + +-- Check patch versioning is in place. +SELECT _v.register_patch('stasis-0001', NULL, NULL); + + +CREATE TABLE IF NOT EXISTS anastasis_truth_payment + (truth_uuid BYTEA PRIMARY KEY CHECK(LENGTH(truth_uuid)=32), + amount_val INT8 NOT NULL, + amount_frac INT4 NOT NULL, + expiration INT8 NOT NULL); +COMMENT ON TABLE anastasis_truth_payment + IS 'Records about payments for truth uploads'; +COMMENT ON COLUMN anastasis_truth_payment.truth_uuid + IS 'Identifier of the truth'; +COMMENT ON COLUMN anastasis_truth_payment.amount_val + IS 'Amount we were paid'; +COMMENT ON COLUMN anastasis_truth_payment.amount_frac + IS 'Amount we were paid fraction'; +COMMENT ON COLUMN anastasis_truth_payment.expiration + IS 'At which date will the truth payment expire'; + + +CREATE TABLE IF NOT EXISTS anastasis_truth + (truth_uuid BYTEA PRIMARY KEY CHECK(LENGTH(truth_uuid)=32), + key_share_data BYTEA CHECK(LENGTH(key_share_data)=80) NOT NULL, + method_name VARCHAR NOT NULL, + encrypted_truth BYTEA NOT NULL, + truth_mime VARCHAR NOT NULL, + expiration INT8 NOT NULL); +COMMENT ON TABLE anastasis_truth + IS 'Truth data is needed to authenticate clients during recovery'; +COMMENT ON COLUMN anastasis_truth.truth_uuid + IS 'The truth UUID uniquely identifies this truth record. Not a foreign key as we may offer storing truth for free.'; +COMMENT ON COLUMN anastasis_truth.key_share_data + IS 'Stores the encrypted key share used to recover the key (nonce, tag and keyshare)'; +COMMENT ON COLUMN anastasis_truth.method_name + IS 'Defines the authentication method (SMS, E-Mail, Question..)'; +COMMENT ON COLUMN anastasis_truth.encrypted_truth + IS 'Stores the encrypted authentication data'; +COMMENT ON COLUMN anastasis_truth.truth_mime + IS 'Defines the mime type of the stored authentcation data'; +COMMENT ON COLUMN anastasis_truth.expiration + IS 'At which date will the truth record expire'; + + +CREATE TABLE IF NOT EXISTS anastasis_user + (user_id BYTEA PRIMARY KEY CHECK(LENGTH(user_id)=32), + expiration_date INT8 NOT NULL); +COMMENT ON TABLE anastasis_user + IS 'Saves a user which is using Anastasis'; +COMMENT ON COLUMN anastasis_user.user_id + IS 'Identifier of the user account'; +COMMENT ON COLUMN anastasis_user.expiration_date + IS 'At which date will the user record expire'; + + +CREATE TABLE IF NOT EXISTS anastasis_recdoc_payment + (payment_id BIGSERIAL PRIMARY KEY, + user_id BYTEA NOT NULL REFERENCES anastasis_user(user_id), + post_counter INT4 NOT NULL DEFAULT 0 CHECK(post_counter >= 0), + amount_val INT8 NOT NULL, + amount_frac INT4 NOT NULL, + payment_identifier BYTEA NOT NULL CHECK(LENGTH(payment_identifier)=32), + creation_date INT8 NOT NULL, + paid BOOLEAN NOT NULL DEFAULT FALSE); +COMMENT ON TABLE anastasis_recdoc_payment + IS 'Records a payment for a recovery document'; +COMMENT ON COLUMN anastasis_recdoc_payment.payment_id + IS 'Serial number which identifies the payment'; +COMMENT ON COLUMN anastasis_recdoc_payment.user_id + IS 'Link to the corresponding user who paid'; +COMMENT ON COLUMN anastasis_recdoc_payment.post_counter + IS 'For how many posts does the user pay'; +COMMENT ON COLUMN anastasis_recdoc_payment.amount_val + IS 'Amount we were paid'; +COMMENT ON COLUMN anastasis_recdoc_payment.amount_frac + IS 'Amount we were paid fraction'; +COMMENT ON COLUMN anastasis_recdoc_payment.payment_identifier + IS 'Payment identifier which the user has to provide'; +COMMENT ON COLUMN anastasis_recdoc_payment.creation_date + IS 'Creation date of the payment'; +COMMENT ON COLUMN anastasis_recdoc_payment.paid + IS 'Is the payment finished'; + + +CREATE TABLE IF NOT EXISTS anastasis_challenge_payment + (payment_id BIGSERIAL PRIMARY KEY, + truth_uuid BYTEA CHECK(LENGTH(truth_uuid)=32) NOT NULL, + amount_val INT8 NOT NULL, + amount_frac INT4 NOT NULL, + payment_identifier BYTEA NOT NULL CHECK(LENGTH(payment_identifier)=32), + creation_date INT8 NOT NULL, + counter INT4 NOT NULL DEFAULT 3, + paid BOOLEAN NOT NULL DEFAULT FALSE, + refunded BOOLEAN NOT NULL DEFAULT FALSE + ); +COMMENT ON TABLE anastasis_recdoc_payment + IS 'Records a payment for a challenge'; +COMMENT ON COLUMN anastasis_challenge_payment.payment_id + IS 'Serial number which identifies the payment'; +COMMENT ON COLUMN anastasis_challenge_payment.truth_uuid + IS 'Link to the corresponding challenge which is paid'; +COMMENT ON COLUMN anastasis_challenge_payment.amount_val + IS 'Amount we were paid'; +COMMENT ON COLUMN anastasis_challenge_payment.amount_frac + IS 'Amount we were paid fraction'; +COMMENT ON COLUMN anastasis_challenge_payment.payment_identifier + IS 'Payment identifier which the user has to provide'; +COMMENT ON COLUMN anastasis_challenge_payment.counter + IS 'How many more times will we issue the challenge for the given payment'; +COMMENT ON COLUMN anastasis_challenge_payment.creation_date + IS 'Creation date of the payment'; +COMMENT ON COLUMN anastasis_challenge_payment.paid + IS 'Is the payment finished'; +COMMENT ON COLUMN anastasis_challenge_payment.refunded + IS 'Was the payment refunded'; + + +CREATE TABLE IF NOT EXISTS anastasis_recoverydocument + (user_id BYTEA NOT NULL REFERENCES anastasis_user(user_id), + version INT4 NOT NULL, + account_sig BYTEA NOT NULL CHECK(LENGTH(account_sig)=64), + recovery_data_hash BYTEA NOT NULL CHECK(length(recovery_data_hash)=64), + recovery_data BYTEA NOT NULL, + PRIMARY KEY (user_id, version)); +COMMENT ON TABLE anastasis_recoverydocument + IS 'Stores a recovery document which contains the policy and the encrypted core secret'; +COMMENT ON COLUMN anastasis_recoverydocument.user_id + IS 'Link to the owner of this recovery document'; +COMMENT ON COLUMN anastasis_recoverydocument.version + IS 'The version of this recovery document'; +COMMENT ON COLUMN anastasis_recoverydocument.account_sig + IS 'Signature of the recovery document'; +COMMENT ON COLUMN anastasis_recoverydocument.recovery_data_hash + IS 'Hash of the recovery document to prevent unnecessary uploads'; +COMMENT ON COLUMN anastasis_recoverydocument.recovery_data + IS 'Contains the encrypted policy and core secret'; + + +CREATE TABLE IF NOT EXISTS anastasis_challengecode + (truth_uuid BYTEA CHECK(LENGTH(truth_uuid)=32) NOT NULL, + code INT8 NOT NULL, + creation_date INT8 NOT NULL, + expiration_date INT8 NOT NULL, + retransmission_date INT8 NOT NULL DEFAULT 0, + retry_counter INT4 NOT NULL); +COMMENT ON TABLE anastasis_challengecode + IS 'Stores a code which is checked for the authentication by SMS, E-Mail..'; +COMMENT ON COLUMN anastasis_challengecode.truth_uuid + IS 'Link to the corresponding challenge which is solved'; +COMMENT ON COLUMN anastasis_challengecode.code + IS 'The pin code which is sent to the user and verified'; +COMMENT ON COLUMN anastasis_challengecode.creation_date + IS 'Creation date of the code'; +COMMENT ON COLUMN anastasis_challengecode.retransmission_date + IS 'When did we last transmit the challenge to the user'; +COMMENT ON COLUMN anastasis_challengecode.expiration_date + IS 'When will the code expire'; +COMMENT ON COLUMN anastasis_challengecode.retry_counter + IS 'How many tries are left for this code must be > 0'; + +CREATE INDEX IF NOT EXISTS anastasis_challengecode_uuid_index + ON anastasis_challengecode + (truth_uuid,expiration_date); +COMMENT ON INDEX anastasis_challengecode_uuid_index + IS 'for challenge lookup'; + +CREATE INDEX IF NOT EXISTS anastasis_challengecode_expiration_index + ON anastasis_challengecode + (truth_uuid,expiration_date); +COMMENT ON INDEX anastasis_challengecode_expiration_index + IS 'for challenge garbage collection'; + + +-- Complete transaction +COMMIT; diff --git a/src/stasis/stasis-postgres.conf b/src/stasis/stasis-postgres.conf new file mode 100644 index 0000000..0d9b209 --- /dev/null +++ b/src/stasis/stasis-postgres.conf @@ -0,0 +1,6 @@ +[stasis-postgres] +CONFIG = "postgres:///anastasis" + +# Where are the SQL files to setup our tables? +# Important: this MUST end with a "/"! +SQL_DIR = $DATADIR/sql/ diff --git a/src/stasis/test_anastasis_db.c b/src/stasis/test_anastasis_db.c new file mode 100644 index 0000000..5e21530 --- /dev/null +++ b/src/stasis/test_anastasis_db.c @@ -0,0 +1,344 @@ +/* + 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 +*/ +/** + * @file anastasis/test_anastasis_db.c + * @brief testcase for anastasis postgres db plugin + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_database_lib.h" +#include "anastasis_util_lib.h" +#include + + +#define FAILIF(cond) \ + do { \ + if (! (cond)) { break;} \ + GNUNET_break (0); \ + goto drop; \ + } while (0) + +#define RND_BLK(ptr) \ + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, ptr, sizeof (*ptr)) + +/** + * Global return value for the test. Initially -1, set to 0 upon + * completion. Other values indicate some kind of error. + */ +static int result; + +/** + * Handle to the plugin we are testing. + */ +static struct ANASTASIS_DatabasePlugin *plugin; + + +/** + * Main function that will be run by the scheduler. + * + * @param cls closure with config + */ +static void +run (void *cls) +{ + struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct TALER_Amount amount; + struct ANASTASIS_PaymentSecretP paymentSecretP; + struct ANASTASIS_CRYPTO_AccountPublicKeyP accountPubP; + struct ANASTASIS_AccountSignatureP accountSig; + struct ANASTASIS_AccountSignatureP res_account_sig; + struct GNUNET_HashCode recoveryDataHash; + struct GNUNET_HashCode res_recovery_data_hash; + struct GNUNET_HashCode r; + struct GNUNET_TIME_Relative rel_time; + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + struct ANASTASIS_CRYPTO_EncryptedKeyShareP key_share; + unsigned int post_counter; + char *mime_type; + char *method; + uint32_t docVersion; + uint32_t res_version; + size_t recoverydatasize; + void *res_recovery_data = NULL; + struct ANASTASIS_CRYPTO_EncryptedKeyShareP res_key_share; + bool paid; + bool valid_counter; + uint32_t recversion = 1; + unsigned char aes_gcm_tag[16]; + const char *recovery_data = "RECOVERY_DATA"; + uint64_t challenge_code = 1234; + struct GNUNET_HashCode c_hash; + struct ANASTASIS_UploadSignaturePS usp = { + .purpose.purpose = htonl (GNUNET_SIGNATURE_PURPOSE_TEST), + .purpose.size = htonl (sizeof (usp)) + }; + + if (NULL == (plugin = ANASTASIS_DB_plugin_load (cfg))) + { + result = 77; + return; + } + if (GNUNET_OK != plugin->drop_tables (plugin->cls)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Dropping tables failed\n"); + result = 77; + return; + } + ANASTASIS_DB_plugin_unload (plugin); + if (NULL == (plugin = ANASTASIS_DB_plugin_load (cfg))) + { + result = 77; + return; + } + + GNUNET_CRYPTO_hash (recovery_data, + strlen (recovery_data), + &recoveryDataHash); + RND_BLK (&paymentSecretP); + RND_BLK (&aes_gcm_tag); + post_counter = 2; + mime_type = "Picture"; + method = "Method"; + TALER_string_to_amount ("EUR:30",&amount); + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &truth_uuid, + sizeof (truth_uuid)); + rel_time = GNUNET_TIME_UNIT_MONTHS; + + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("EUR:1", + &amount)); + + memset (&key_share, 1, sizeof (key_share)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->store_truth (plugin->cls, + &truth_uuid, + &key_share, + mime_type, + "encrypted_truth", + strlen ("encrypted_truth"), + method, + rel_time)); + + FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != + plugin->check_payment_identifier (plugin->cls, + &paymentSecretP, + &paid, + &valid_counter)); + + memset (&accountPubP, 2, sizeof (accountPubP)); + memset (&accountSig, 3, sizeof (accountSig)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->record_recdoc_payment (plugin->cls, + &accountPubP, + post_counter, + &paymentSecretP, + &amount)); + { + struct GNUNET_TIME_Absolute res_time; + + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->increment_lifetime (plugin->cls, + &accountPubP, + &paymentSecretP, + rel_time, + &res_time)); + } + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->check_payment_identifier (plugin->cls, + &paymentSecretP, + &paid, + &valid_counter)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != + plugin->check_challenge_payment (plugin->cls, + &paymentSecretP, + &truth_uuid, + &paid)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->record_challenge_payment (plugin->cls, + &truth_uuid, + &paymentSecretP, + &amount)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->update_challenge_payment (plugin->cls, + &truth_uuid, + &paymentSecretP)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->check_challenge_payment (plugin->cls, + &paymentSecretP, + &truth_uuid, + &paid)); + FAILIF (! paid); + FAILIF (ANASTASIS_DB_STORE_STATUS_SUCCESS != + plugin->store_recovery_document (plugin->cls, + &accountPubP, + &accountSig, + &recoveryDataHash, + recovery_data, + strlen (recovery_data), + &paymentSecretP, + &docVersion)); + { + uint32_t vrs; + struct GNUNET_TIME_Absolute exp; + + FAILIF (ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED != + plugin->lookup_account (plugin->cls, + &accountPubP, + &exp, + &r, + &vrs)); + } + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->get_key_share (plugin->cls, + &truth_uuid, + &res_key_share)); + FAILIF (0 != + GNUNET_memcmp (&res_key_share, + &key_share)); + + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->get_recovery_document (plugin->cls, + &accountPubP, + recversion, + &res_account_sig, + &res_recovery_data_hash, + &recoverydatasize, + &res_recovery_data)); + FAILIF (0 != memcmp (res_recovery_data, + recovery_data, + strlen (recovery_data))); + GNUNET_free (res_recovery_data); + + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->get_latest_recovery_document (plugin->cls, + &accountPubP, + &res_account_sig, + &res_recovery_data_hash, + &recoverydatasize, + &res_recovery_data, + &res_version)); + FAILIF (0 != memcmp (res_recovery_data, + recovery_data, + strlen (recovery_data))); + GNUNET_free (res_recovery_data); + + { + struct GNUNET_TIME_Absolute rt; + + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->create_challenge_code (plugin->cls, + &truth_uuid, + GNUNET_TIME_UNIT_HOURS, + GNUNET_TIME_UNIT_DAYS, + 3, /* retry counter */ + &rt, + &challenge_code)); + FAILIF (0 != rt.abs_value_us); + } + { + struct GNUNET_TIME_Absolute rt; + uint64_t c2; + + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->create_challenge_code (plugin->cls, + &truth_uuid, + GNUNET_TIME_UNIT_HOURS, + GNUNET_TIME_UNIT_DAYS, + 3, /* retry counter */ + &rt, + &c2)); + FAILIF (c2 != challenge_code); + } + ANASTASIS_hash_answer (123, + &c_hash); + FAILIF (ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH != + plugin->verify_challenge_code (plugin->cls, + &truth_uuid, + &c_hash)); + + ANASTASIS_hash_answer (challenge_code, + &c_hash); + FAILIF (ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED != + plugin->verify_challenge_code (plugin->cls, + &truth_uuid, + &c_hash)); + + if (-1 == result) + result = 0; + +drop: + GNUNET_break (GNUNET_OK == + plugin->drop_tables (plugin->cls)); + ANASTASIS_DB_plugin_unload (plugin); + if (NULL != plugin) + { + plugin = NULL; + } +} + + +int +main (int argc, + char *const argv[]) +{ + const char *plugin_name; + char *config_filename; + char *testname; + struct GNUNET_CONFIGURATION_Handle *cfg; + + result = -1; + if (NULL == (plugin_name = strrchr (argv[0], (int) '-'))) + { + GNUNET_break (0); + return -1; + } + /* 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 ()); + GNUNET_log_setup (argv[0], "DEBUG", NULL); + plugin_name++; + GNUNET_asprintf (&testname, + "%s", + plugin_name); + GNUNET_asprintf (&config_filename, + "test_anastasis_db_%s.conf", + testname); + cfg = GNUNET_CONFIGURATION_create (); + if (GNUNET_OK != + GNUNET_CONFIGURATION_load (cfg, + config_filename)) + { + GNUNET_break (0); + GNUNET_free (config_filename); + GNUNET_free (testname); + return 2; + } + GNUNET_SCHEDULER_run (&run, + cfg); + GNUNET_CONFIGURATION_destroy (cfg); + GNUNET_free (config_filename); + GNUNET_free (testname); + return result; +} + + +/* end of test_anastasis_db.c */ diff --git a/src/stasis/test_anastasis_db_postgres.conf b/src/stasis/test_anastasis_db_postgres.conf new file mode 100644 index 0000000..8971a62 --- /dev/null +++ b/src/stasis/test_anastasis_db_postgres.conf @@ -0,0 +1,10 @@ +[anastasis] +#The DB plugin to use +DB = postgres + +[taler] +CURRENCY = EUR + +[stasis-postgres] +#The connection string the plugin has to use for connecting to the database +CONFIG = postgres:///anastasischeck diff --git a/src/testing/.gitignore b/src/testing/.gitignore new file mode 100644 index 0000000..a6eb294 --- /dev/null +++ b/src/testing/.gitignore @@ -0,0 +1,3 @@ +test_anastasis +test_anastasisrest_api +test_anastasis_api_home/.local/share/taler/crypto-* diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am new file mode 100644 index 0000000..8fc710b --- /dev/null +++ b/src/testing/Makefile.am @@ -0,0 +1,91 @@ +# 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 = \ + libanastasistesting.la + +libanastasistesting_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined +libanastasistesting_la_SOURCES = \ + testing_api_cmd_policy_store.c \ + testing_api_cmd_truth_store.c \ + testing_api_cmd_policy_lookup.c \ + testing_api_cmd_keyshare_lookup.c \ + testing_api_cmd_config.c \ + testing_api_helpers.c \ + testing_api_trait_account_pub.c \ + testing_api_trait_account_priv.c \ + testing_api_trait_eks.c \ + testing_api_trait_payment_secret.c \ + testing_api_trait_truth_key.c \ + testing_api_trait_truth_uuid.c \ + testing_api_trait_hash.c \ + testing_api_trait_salt.c \ + testing_api_trait_code.c \ + testing_cmd_truth_upload.c \ + testing_cmd_policy_create.c \ + testing_cmd_secret_share.c \ + testing_cmd_recover_secret.c \ + testing_cmd_challenge_answer.c \ + testing_trait_truth.c \ + testing_trait_policy.c \ + testing_trait_core_secret.c \ + testing_trait_challenge.c +libanastasistesting_la_LIBADD = \ + $(top_builddir)/src/restclient/libanastasisrest.la \ + $(top_builddir)/src/lib/libanastasis.la \ + $(top_builddir)/src/util/libanastasisutil.la \ + -ltalerexchange \ + -ltalermerchant \ + -ltalerjson \ + -ltalerutil \ + -lgnunetcurl \ + -lgnunetjson \ + -lgnunetutil \ + -ljansson \ + -luuid \ + -ltalertesting \ + $(XLIB) + + +check_PROGRAMS = \ + test_anastasisrest_api \ + test_anastasis + +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_PROGRAMS) + +test_anastasisrest_api_SOURCES = \ + test_anastasis_api.c +test_anastasisrest_api_LDADD = \ + libanastasistesting.la \ + -ltalermerchanttesting \ + -ltalertesting \ + -lgnunetutil \ + $(XLIB) + +test_anastasis_SOURCES = \ + test_anastasis.c +test_anastasis_LDADD = \ + libanastasistesting.la \ + -ltalermerchanttesting \ + -ltalertesting \ + -ltalerexchange \ + -lgnunetutil \ + $(XLIB) + +EXTRA_DIST = \ + test_anastasis_api.conf \ + test_anastasis_api_home/.config/taler/exchange/account-2.json \ + test_anastasis_api_home/.local/share/taler/exchange/offline-keys/master.priv \ + sms_authentication.sh + +MOSTLYCLEANFILES = \ + test_anastasis_api_home/.local/share/taler/exchange/offline-keys/secm_tofus.pub diff --git a/src/testing/sms_authentication.sh b/src/testing/sms_authentication.sh new file mode 100755 index 0000000..0c2c851 --- /dev/null +++ b/src/testing/sms_authentication.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Note: to use this script, you must set +# ANASTASIS_SMS_API_KEY and +# ANASTASIS_SMS_API_SECRET environment variables. +curl -X "POST" "https://rest.nexmo.com/sms/json" \ + -d "from=Vonage APIs" \ + -d "text=$1" \ + -d "to=$2" \ + -d "api_key=$ANASTASIS_SMS_API_KEY" \ + -d "api_secret=$ANASTASIS_SMS_API_SECRET" diff --git a/src/testing/test_anastasis.c b/src/testing/test_anastasis.c new file mode 100644 index 0000000..2ff7cb0 --- /dev/null +++ b/src/testing/test_anastasis.c @@ -0,0 +1,464 @@ +/* + 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 +*/ +/** + * @file lib/test_anastasis.c + * @brief testcase to test anastasis + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_testing_lib.h" +#include + + +/** + * Configuration file we use. One (big) configuration is used + * for the various components for this test. + */ +#define CONFIG_FILE "test_anastasis_api.conf" + +/** + * Exchange base URL. Could also be taken from config. + */ +#define EXCHANGE_URL "http://localhost:8081/" + +/** + * Account number of the exchange at the bank. + */ +#define EXCHANGE_ACCOUNT_NAME "2" + +/** + * Account number of some user. + */ +#define USER_ACCOUNT_NAME "62" + +/** + * Account number used by the merchant + */ +#define MERCHANT_ACCOUNT_NAME "3" + +/** + * Configuration of the bank. + */ +static struct TALER_TESTING_BankConfiguration bc; + +/** + * Configuration of the exchange. + */ +static struct TALER_TESTING_ExchangeConfiguration ec; + +/** + * Payto URI of the customer (payer). + */ +static char *payer_payto; + +/** + * Payto URI of the exchange (escrow account). + */ +static char *exchange_payto; + +/** + * Payto URI of the merchant (receiver). + */ +static char *merchant_payto; + +/** + * Merchant base URL. + */ +static char *merchant_url; + +/** + * Anastasis base URL. + */ +static char *anastasis_url; + +/** + * Name of the file for exchanging the secret. + */ +static char *file_secret; + +/** + * Merchant process. + */ +static struct GNUNET_OS_Process *merchantd; + +/** + * Anastasis process. + */ +static struct GNUNET_OS_Process *anastasisd; + +/** + * Identity to use for testing. + */ +static json_t *id_data; + + +/** + * Execute the taler-exchange-wirewatch command with + * our configuration file. + * + * @param label label to use for the command. + */ +static struct TALER_TESTING_Command +cmd_exec_wirewatch (char *label) +{ + return TALER_TESTING_cmd_exec_wirewatch (label, + CONFIG_FILE); +} + + +/** + * Run wire transfer of funds from some user's account to the + * exchange. + * + * @param label label to use for the command. + * @param amount amount to transfer, i.e. "EUR:1" + * @param url exchange_url + */ +static struct TALER_TESTING_Command +cmd_transfer_to_exchange (const char *label, + const char *amount) +{ + return TALER_TESTING_cmd_admin_add_incoming (label, + amount, + &bc.exchange_auth, + payer_payto); +} + + +/** + * Main function that will tell the interpreter what commands to + * run. + * + * @param cls closure + */ +static void +run (void *cls, + struct TALER_TESTING_Interpreter *is) +{ + struct TALER_TESTING_Command pay[] = { + /** + * Move money to the exchange's bank account. + */ + cmd_transfer_to_exchange ("create-reserve-1", + "EUR:10.02"), + /** + * Make a reserve exist, according to the previous + * transfer. + */ + cmd_exec_wirewatch ("wirewatch-1"), + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-1", + "create-reserve-1", + "EUR:5", + MHD_HTTP_OK), + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-2", + "create-reserve-1", + "EUR:5", + MHD_HTTP_OK), + /** + * Check the reserve is depleted. + */ + TALER_TESTING_cmd_status ("withdraw-status-1", + "create-reserve-1", + "EUR:0", + MHD_HTTP_OK), + TALER_TESTING_cmd_end () + }; + + struct TALER_TESTING_Command anastasis[] = { + ANASTASIS_TESTING_cmd_config ("salt-request-1", + anastasis_url, + MHD_HTTP_OK), + ANASTASIS_TESTING_cmd_truth_upload_question ("truth-create-1", + anastasis_url, + id_data, + "answer the question", + "text/plain", + "SomeTruth1", + MHD_HTTP_NO_CONTENT, + ANASTASIS_TESTING_TSO_NONE, + "salt-request-1"), + ANASTASIS_TESTING_cmd_truth_upload_question ("truth-create-2", + anastasis_url, + id_data, + "answer the question", + "text/plain", + "SomeTruth2", + MHD_HTTP_NO_CONTENT, + ANASTASIS_TESTING_TSO_NONE, + "salt-request-1"), + ANASTASIS_TESTING_cmd_truth_upload ("truth-create-3", + anastasis_url, + id_data, + "file", + "read the file", + "text/plain", + file_secret, + strlen (file_secret), + MHD_HTTP_NO_CONTENT, + ANASTASIS_TESTING_TSO_NONE, + "salt-request-1"), + ANASTASIS_TESTING_cmd_policy_create ("policy-create-1", + "truth-create-1", + "truth-create-2", + NULL), + ANASTASIS_TESTING_cmd_policy_create ("policy-create-2", + "truth-create-1", + "truth-create-3", + NULL), + ANASTASIS_TESTING_cmd_policy_create ("policy-create-3", + "truth-create-2", + "truth-create-3", + NULL), + ANASTASIS_TESTING_cmd_secret_share ("secret-share-1", + anastasis_url, + "salt-request-1", + NULL, + id_data, + "core secret", + strlen ("core secret"), + ANASTASIS_SHARE_STATUS_PAYMENT_REQUIRED, + ANASTASIS_TESTING_SSO_NONE, + "policy-create-1", + "policy-create-2", + "policy-create-3", + NULL), + /* what would we have to pay? */ + TALER_TESTING_cmd_merchant_claim_order ("fetch-proposal", + merchant_url, + MHD_HTTP_OK, + "secret-share-1", + NULL), + /* make the payment */ + TALER_TESTING_cmd_merchant_pay_order ("pay-account", + merchant_url, + MHD_HTTP_OK, + "fetch-proposal", + "withdraw-coin-1", + "EUR:5", + "EUR:4.99", /* must match ANNUAL_FEE in config! */ + NULL), + ANASTASIS_TESTING_cmd_secret_share ("secret-share-2", + anastasis_url, + "salt-request-1", + "secret-share-1", + id_data, + "core secret", + strlen ("core secret"), + ANASTASIS_SHARE_STATUS_SUCCESS, + ANASTASIS_TESTING_SSO_NONE, + "policy-create-1", + "policy-create-2", + "policy-create-3", + NULL), + ANASTASIS_TESTING_cmd_recover_secret ("recover-secret-1", + anastasis_url, + id_data, + 0, /* version */ + ANASTASIS_TESTING_RSO_NONE, + "salt-request-1", + "secret-share-2"), + ANASTASIS_TESTING_cmd_challenge_answer ("challenge-answer-1", + NULL, /* payment ref */ + "recover-secret-1", /* challenge ref */ + 0, /* challenge index */ + "SomeTruth1", + 0, /* mode */ + ANASTASIS_CHALLENGE_STATUS_SOLVED), +#if 0 + ANASTASIS_TESTING_cmd_challenge_answer ("challenge-answer-2", + NULL, /* payment ref */ + "recover-secret-1", + 1, /* challenge index */ + "SomeTruth2", + 0, /* mode */ + ANASTASIS_CHALLENGE_STATUS_SOLVED), +#endif + ANASTASIS_TESTING_cmd_challenge_start ("challenge-start-3-pay", + NULL, /* payment ref */ + "recover-secret-1", + 2, /* challenge index */ + ANASTASIS_CHALLENGE_STATUS_PAYMENT_REQUIRED), + TALER_TESTING_cmd_merchant_claim_order ("fetch-challenge-pay-proposal", + merchant_url, + MHD_HTTP_OK, + "challenge-start-3-pay", + NULL), + TALER_TESTING_cmd_merchant_pay_order ("pay-file-challenge", + merchant_url, + MHD_HTTP_OK, + "fetch-challenge-pay-proposal", + "withdraw-coin-2", + "EUR:1", + "EUR:1", /* must match COST in config! */ + NULL), + ANASTASIS_TESTING_cmd_challenge_start ("challenge-start-3-paid", + "challenge-start-3-pay", /* payment ref */ + "recover-secret-1", + 2, /* challenge index */ + ANASTASIS_CHALLENGE_STATUS_INSTRUCTIONS), + ANASTASIS_TESTING_cmd_challenge_answer ("challenge-answer-3", + "challenge-start-3-pay", /* payment ref */ + "recover-secret-1", + 2, /* challenge index */ + "challenge-start-3-paid", /* answer */ + 1, /* mode */ + ANASTASIS_CHALLENGE_STATUS_SOLVED), + ANASTASIS_TESTING_cmd_recover_secret_finish ("recover-finish-1", + "recover-secret-1", + GNUNET_TIME_UNIT_SECONDS), + TALER_TESTING_cmd_end () + }; + + struct TALER_TESTING_Command commands[] = { + /* general setup */ + TALER_TESTING_cmd_auditor_add ("add-auditor-OK", + MHD_HTTP_NO_CONTENT, + false), + TALER_TESTING_cmd_wire_add ("add-wire-account", + "payto://x-taler-bank/localhost/2", + MHD_HTTP_NO_CONTENT, + false), + TALER_TESTING_cmd_exec_offline_sign_keys ("offline-sign-future-keys", + CONFIG_FILE), + TALER_TESTING_cmd_exec_offline_sign_fees ("offline-sign-fees", + CONFIG_FILE, + "EUR:0.01", + "EUR:0.01"), + TALER_TESTING_cmd_check_keys_pull_all_keys ("refetch /keys", + 1), + TALER_TESTING_cmd_merchant_post_instances ("instance-create-default", + merchant_url, + "default", + merchant_payto, + "EUR", + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_batch ("pay", + pay), + TALER_TESTING_cmd_batch ("anastasis", + anastasis), + TALER_TESTING_cmd_end () + }; + + TALER_TESTING_run_with_fakebank (is, + commands, + bc.exchange_auth.wire_gateway_url); +} + + +int +main (int argc, + char *const *argv) +{ + unsigned int ret; + /* These environment variables get in the way... */ + unsetenv ("XDG_DATA_HOME"); + unsetenv ("XDG_CONFIG_HOME"); + + GNUNET_log_setup ("test-anastasis", + "DEBUG", + NULL); + if (GNUNET_OK != + TALER_TESTING_prepare_fakebank (CONFIG_FILE, + "exchange-account-exchange", + &bc)) + return 77; + { + char dir[] = "/tmp/test-anastasis-file-XXXXXX"; + + if (NULL == mkdtemp (dir)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "mkdtemp", + dir); + return 77; + } + GNUNET_asprintf (&file_secret, + "%s/.secret", + dir); + } + id_data = ANASTASIS_TESTING_make_id_data_example ( + "MaxMuster123456789"); + payer_payto = ("payto://x-taler-bank/localhost/" USER_ACCOUNT_NAME); + exchange_payto = ("payto://x-taler-bank/localhost/" EXCHANGE_ACCOUNT_NAME); + merchant_payto = ("payto://x-taler-bank/localhost/" MERCHANT_ACCOUNT_NAME); + if (NULL == + (merchant_url = TALER_TESTING_prepare_merchant (CONFIG_FILE))) + return 77; + TALER_TESTING_cleanup_files (CONFIG_FILE); + + if (NULL == + (anastasis_url = ANASTASIS_TESTING_prepare_anastasis (CONFIG_FILE))) + return 77; + TALER_TESTING_cleanup_files (CONFIG_FILE); + + switch (TALER_TESTING_prepare_exchange (CONFIG_FILE, + GNUNET_YES, + &ec)) + { + case GNUNET_SYSERR: + GNUNET_break (0); + return 1; + case GNUNET_NO: + return 77; + case GNUNET_OK: + if (NULL == (merchantd = + TALER_TESTING_run_merchant (CONFIG_FILE, + merchant_url))) + { + GNUNET_break (0); + return 1; + } + if (NULL == (anastasisd = + ANASTASIS_TESTING_run_anastasis (CONFIG_FILE, + anastasis_url))) + { + GNUNET_break (0); + GNUNET_OS_process_kill (merchantd, + SIGTERM); + GNUNET_OS_process_wait (merchantd); + GNUNET_OS_process_destroy (merchantd); + + return 1; + } + ret = TALER_TESTING_setup_with_exchange (&run, + NULL, + CONFIG_FILE); + + GNUNET_OS_process_kill (merchantd, + SIGTERM); + GNUNET_OS_process_kill (anastasisd, + SIGTERM); + GNUNET_OS_process_wait (merchantd); + GNUNET_OS_process_wait (anastasisd); + GNUNET_OS_process_destroy (merchantd); + GNUNET_OS_process_destroy (anastasisd); + GNUNET_free (merchant_url); + GNUNET_free (anastasis_url); + + if (GNUNET_OK != ret) + return 1; + break; + default: + GNUNET_break (0); + return 1; + } + return 0; +} + + +/* end of test_anastasis.c */ diff --git a/src/testing/test_anastasis_api.c b/src/testing/test_anastasis_api.c new file mode 100644 index 0000000..db18b41 --- /dev/null +++ b/src/testing/test_anastasis_api.c @@ -0,0 +1,421 @@ +/* + 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 +*/ +/** + * @file lib/test_anastasis_api.c + * @brief testcase to test anastasis' HTTP API interface + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_testing_lib.h" +#include + + +/** + * Configuration file we use. One (big) configuration is used + * for the various components for this test. + */ +#define CONFIG_FILE "test_anastasis_api.conf" + +/** + * Exchange base URL. Could also be taken from config. + */ +#define EXCHANGE_URL "http://localhost:8081/" + +/** + * Account number of the exchange at the bank. + */ +#define EXCHANGE_ACCOUNT_NAME "2" + +/** + * Account number of some user. + */ +#define USER_ACCOUNT_NAME "62" + +/** + * Account number used by the merchant + */ +#define MERCHANT_ACCOUNT_NAME "3" + +/** + * Configuration of the bank. + */ +static struct TALER_TESTING_BankConfiguration bc; + +/** + * Configuration of the exchange. + */ +static struct TALER_TESTING_ExchangeConfiguration ec; + +/** + * Payto URI of the customer (payer). + */ +static char *payer_payto; + +/** + * Payto URI of the exchange (escrow account). + */ +static char *exchange_payto; + +/** + * Payto URI of the merchant (receiver). + */ +static char *merchant_payto; + +/** + * Merchant base URL. + */ +static char *merchant_url; + +/** + * Anastasis base URL. + */ +static char *anastasis_url; + +/** + * Merchant process. + */ +static struct GNUNET_OS_Process *merchantd; + +/** + * Anastasis process. + */ +static struct GNUNET_OS_Process *anastasisd; + +/** + * Name of the file for exchanging the secret. + */ +static char *file_secret; + +/** + * Execute the taler-exchange-wirewatch command with our configuration + * file. + * + * @param label label to use for the command. + */ +static struct TALER_TESTING_Command +cmd_exec_wirewatch (char *label) +{ + return TALER_TESTING_cmd_exec_wirewatch (label, + CONFIG_FILE); +} + + +/** + * Run wire transfer of funds from some user's account to the + * exchange. + * + * @param label label to use for the command. + * @param amount amount to transfer, i.e. "EUR:1" + * @param url exchange_url + */ +static struct TALER_TESTING_Command +cmd_transfer_to_exchange (const char *label, + const char *amount) +{ + return TALER_TESTING_cmd_admin_add_incoming (label, + amount, + &bc.exchange_auth, + payer_payto); +} + + +/** + * Main function that will tell the interpreter what commands to run. + * + * @param cls closure + */ +static void +run (void *cls, + struct TALER_TESTING_Interpreter *is) +{ + struct TALER_TESTING_Command withdraw[] = { + cmd_transfer_to_exchange ("create-reserve-1", + "EUR:10.02"), + cmd_exec_wirewatch ("wirewatch-1"), + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-1", + "create-reserve-1", + "EUR:5", + MHD_HTTP_OK), + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-2", + "create-reserve-1", + "EUR:5", + MHD_HTTP_OK), + TALER_TESTING_cmd_status ("withdraw-status-1", + "create-reserve-1", + "EUR:0", + MHD_HTTP_OK), + TALER_TESTING_cmd_end () + }; + + struct TALER_TESTING_Command policy[] = { + ANASTASIS_TESTING_cmd_policy_store ("policy-store-1", + anastasis_url, + NULL /* prev upload */, + MHD_HTTP_PAYMENT_REQUIRED, + ANASTASIS_TESTING_PSO_NONE, + "Test-1", + strlen ("Test-1")), + /* what would we have to pay? */ + TALER_TESTING_cmd_merchant_claim_order ("fetch-proposal", + merchant_url, + MHD_HTTP_OK, + "policy-store-1", + NULL), + /* make the payment */ + TALER_TESTING_cmd_merchant_pay_order ("pay-account", + merchant_url, + MHD_HTTP_OK, + "fetch-proposal", + "withdraw-coin-1", + "EUR:5", + "EUR:4.99", /* must match ANNUAL_FEE in config! */ + NULL), + ANASTASIS_TESTING_cmd_policy_store ("policy-store-2", + anastasis_url, + "policy-store-1", + MHD_HTTP_NO_CONTENT, + ANASTASIS_TESTING_PSO_NONE, + "Test-1", + strlen ("Test-1")), + ANASTASIS_TESTING_cmd_policy_lookup ("policy-lookup-1", + anastasis_url, + MHD_HTTP_OK, + "policy-store-2"), + TALER_TESTING_cmd_end () + }; + + struct TALER_TESTING_Command truth[] = { + ANASTASIS_TESTING_cmd_truth_question ( + "truth-store-1", + anastasis_url, + NULL, + "The-Answer", + ANASTASIS_TESTING_TSO_NONE, + MHD_HTTP_NO_CONTENT), + ANASTASIS_TESTING_cmd_keyshare_lookup ( + "keyshare-lookup-1", + anastasis_url, + "The-Answer", + NULL, /* payment ref */ + "truth-store-1", + 0, + ANASTASIS_KSD_SUCCESS), + ANASTASIS_TESTING_cmd_truth_store ( + "truth-store-2", + anastasis_url, + NULL, + "file", + "text/plain", + strlen (file_secret), + file_secret, + ANASTASIS_TESTING_TSO_NONE, + MHD_HTTP_NO_CONTENT), + ANASTASIS_TESTING_cmd_keyshare_lookup ( + "challenge-fail-1", + anastasis_url, + "Wrong-Answer", + NULL, + "truth-store-1", + 0, + ANASTASIS_KSD_INVALID_ANSWER), + ANASTASIS_TESTING_cmd_keyshare_lookup ( + "file-challenge-run-1", + anastasis_url, + NULL, /* no answer */ + NULL, /* payment ref */ + "truth-store-2", /* upload ref */ + 0, + ANASTASIS_KSD_PAYMENT_REQUIRED), + /* what would we have to pay? */ + TALER_TESTING_cmd_merchant_claim_order ("fetch-proposal-2", + merchant_url, + MHD_HTTP_OK, + "file-challenge-run-1", + NULL), + /* make the payment */ + TALER_TESTING_cmd_merchant_pay_order ("pay-account-2", + merchant_url, + MHD_HTTP_OK, + "fetch-proposal-2", + "withdraw-coin-2", + "EUR:1.01", + "EUR:1", + NULL), + + ANASTASIS_TESTING_cmd_keyshare_lookup ( + "file-challenge-run-2", + anastasis_url, + NULL, /* no answer */ + "file-challenge-run-1", /* payment ref */ + "truth-store-2", + 0, + ANASTASIS_KSD_INVALID_ANSWER), + ANASTASIS_TESTING_cmd_keyshare_lookup ( + "file-challenge-run-3", + anastasis_url, + "file-challenge-run-2", /* answer */ + "file-challenge-run-1", /* payment ref */ + "truth-store-2", + 1, + ANASTASIS_KSD_SUCCESS), + TALER_TESTING_cmd_end () + }; + + struct TALER_TESTING_Command commands[] = { + /* general setup */ + TALER_TESTING_cmd_auditor_add ("add-auditor-OK", + MHD_HTTP_NO_CONTENT, + false), + TALER_TESTING_cmd_wire_add ("add-wire-account", + "payto://x-taler-bank/localhost/2", + MHD_HTTP_NO_CONTENT, + false), + TALER_TESTING_cmd_exec_offline_sign_keys ("offline-sign-future-keys", + CONFIG_FILE), + TALER_TESTING_cmd_exec_offline_sign_fees ("offline-sign-fees", + CONFIG_FILE, + "EUR:0.01", + "EUR:0.01"), + TALER_TESTING_cmd_check_keys_pull_all_keys ("refetch /keys", + 1), + TALER_TESTING_cmd_merchant_post_instances ("instance-create-default", + merchant_url, + "default", + merchant_payto, + "EUR", + MHD_HTTP_NO_CONTENT), + ANASTASIS_TESTING_cmd_config ("salt-request-1", + anastasis_url, + MHD_HTTP_OK), + TALER_TESTING_cmd_batch ("withdraw", + withdraw), + TALER_TESTING_cmd_batch ("policy", + policy), + TALER_TESTING_cmd_batch ("truth", + truth), + TALER_TESTING_cmd_end () + }; + + TALER_TESTING_run_with_fakebank (is, + commands, + bc.exchange_auth.wire_gateway_url); +} + + +int +main (int argc, + char *const *argv) +{ + int ret; + + /* These environment variables get in the way... */ + unsetenv ("XDG_DATA_HOME"); + unsetenv ("XDG_CONFIG_HOME"); + GNUNET_log_setup ("test-anastasis-api", + "DEBUG", + NULL); + if (GNUNET_OK != + TALER_TESTING_prepare_fakebank (CONFIG_FILE, + "exchange-account-exchange", + &bc)) + return 77; + { + char dir[] = "/tmp/test-anastasis-file-XXXXXX"; + + if (NULL == mkdtemp (dir)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "mkdtemp", + dir); + return 77; + } + GNUNET_asprintf (&file_secret, + "%s/.secret", + dir); + } + payer_payto = ("payto://x-taler-bank/localhost/" USER_ACCOUNT_NAME); + exchange_payto = ("payto://x-taler-bank/localhost/" EXCHANGE_ACCOUNT_NAME); + merchant_payto = ("payto://x-taler-bank/localhost/" MERCHANT_ACCOUNT_NAME); + if (NULL == + (merchant_url = TALER_TESTING_prepare_merchant (CONFIG_FILE))) + return 77; + TALER_TESTING_cleanup_files (CONFIG_FILE); + + if (NULL == + (anastasis_url = ANASTASIS_TESTING_prepare_anastasis (CONFIG_FILE))) + return 77; + TALER_TESTING_cleanup_files (CONFIG_FILE); + + switch (TALER_TESTING_prepare_exchange (CONFIG_FILE, + GNUNET_YES, + &ec)) + { + case GNUNET_SYSERR: + GNUNET_break (0); + return 1; + case GNUNET_NO: + return 77; + case GNUNET_OK: + if (NULL == (merchantd = + TALER_TESTING_run_merchant (CONFIG_FILE, + merchant_url))) + { + GNUNET_break (0); + return 1; + } + if (NULL == (anastasisd = + ANASTASIS_TESTING_run_anastasis (CONFIG_FILE, + anastasis_url))) + { + GNUNET_break (0); + GNUNET_OS_process_kill (merchantd, + SIGTERM); + GNUNET_OS_process_wait (merchantd); + GNUNET_OS_process_destroy (merchantd); + return 1; + } + ret = TALER_TESTING_setup_with_exchange (&run, + NULL, + CONFIG_FILE); + GNUNET_OS_process_kill (merchantd, + SIGTERM); + GNUNET_OS_process_kill (anastasisd, + SIGTERM); + GNUNET_OS_process_wait (merchantd); + GNUNET_OS_process_wait (anastasisd); + GNUNET_OS_process_destroy (merchantd); + GNUNET_OS_process_destroy (anastasisd); + GNUNET_free (merchant_url); + GNUNET_free (anastasis_url); + + if (GNUNET_OK != ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Test failed in interpreter\n"); + return 1; + } + break; + default: + GNUNET_break (0); + return 1; + } + return 0; +} + + +/* end of test_anastasis_api.c */ diff --git a/src/testing/test_anastasis_api.conf b/src/testing/test_anastasis_api.conf new file mode 100644 index 0000000..bde7ee1 --- /dev/null +++ b/src/testing/test_anastasis_api.conf @@ -0,0 +1,264 @@ +# This file is in the public domain. +# +[PATHS] +# Persistent data storage for the testcase +TALER_TEST_HOME = test_anastasis_api_home/ +TALER_RUNTIME_DIR = ${TMPDIR:-${TMP:-/tmp}}/${USER:-}/taler-system-runtime/ + +# Persistent data storage +TALER_DATA_HOME = $TALER_TEST_HOME/.local/share/taler/ + +# Configuration files +TALER_CONFIG_HOME = $TALER_TEST_HOME/.config/taler/ + +# Cached data, no big deal if lost +TALER_CACHE_HOME = $TALER_TEST_HOME/.cache/taler/ + +[taler] +# What currency do we use? +#currency = EUR +currency = EUR +#CURRENCY_ROUND_UNIT = EUR:0.01 +#CURRENCY_ROUND_UNIT = EUR:0.01 + +[taler-helper-crypto-rsa] +# Reduce from 1 year to speed up test +LOOKAHEAD_SIGN = 12 days + +[taler-helper-crypto-eddsa] +# Reduce from 1 year to speed up test +LOOKAHEAD_SIGN = 12 days +# Reduce from 12 weeks to ensure we have multiple +DURATION = 7 days + + +[bank] +HTTP_PORT = 8082 +#BASE_URL = https://bank.test.taler.net/ + +########################################## +# Configuration for Anastasis # +########################################## + +[anastasis] +PORT = 8086 + +DB = postgres + +BUSINESS_NAME = "Checker's Test Inc." + +# Upload limit +UPLOAD_LIMIT_MB = 1 + +ANNUAL_POLICY_UPLOAD_LIMIT = 64 + +INSURANCE = EUR:0 + +SERVER_SALT = salty + +# Base URL of anastasis. +# BASE_URL = http://localhost:8086/ + +# Where does our payment backend run? Must match PORT under [merchant] +PAYMENT_BACKEND_URL = http://localhost:8080/ + +# Annual fee we charge. +#ANNUAL_FEE = EUR:4.99 +ANNUAL_FEE = EUR:4.99 + +TRUTH_UPLOAD_FEE = EUR:0.0 + +# Authentication costs +[authorization-question] +# Cost of authentication by question +COST = EUR:0 + +[authorization-file] +# Cost of authentication by file (only for testing purposes) +COST = EUR:1 + +[authorization-email] +# Cost of authentication by E-Mail +COST = EUR:0 + +[authorization-sms] +# Cost of authentication by SMS +COST = EUR:0 + +# Command which is executed for the sms authentication +COMMAND = ./sms_authentication.sh + + + + +# This specifies which database the postgres backend uses. +[stasis-postgres] +CONFIG = postgres:///anastasischeck + +########################################## +# Configuration for the merchant backend # +########################################## + +[merchant] + +# Which port do we run the backend on? (HTTP server) +PORT = 8080 + +# How quickly do we want the exchange to send us our money? +# Used only if the frontend does not specify a value. +WIRE_TRANSFER_DELAY = 0 s + +# Which plugin (backend) do we use for the DB. +DB = postgres + +# Default choice for maximum wire fee. +DEFAULT_MAX_WIRE_FEE = EUR:0.10 + +# Default choice for maximum deposit fee. +DEFAULT_MAX_DEPOSIT_FEE = EUR:0.10 + + +# This specifies which database the postgres backend uses. +[merchantdb-postgres] +CONFIG = postgres:///talercheck + +# Sections starting with "exchange-" specify trusted exchanges +# (by the merchant) +[merchant-exchange-default] +MASTER_KEY = T1VVFQZZARQ1CMF4BN58EE7SKTW5AV2BS18S87ZEGYS4S29J6DNG +EXCHANGE_BASE_URL = http://localhost:8081/ +#MASTER_KEY = DY95EXAHQ2BKM2WK9YHZHYG1R7PPMMJPY14FNGP662DAKE35AKQG +#EXCHANGE_BASE_URL = https://exchange.test.taler.net/ +#CURRENCY = EUR +CURRENCY = EUR + +# only fixes skips. +[auditor] +BASE_URL = http://the.auditor/ +#BASE_URL = https://auditor.test.taler.net/ +#AUDITOR_KEY = DSDASDXAMDAARMNAD53ZA4AFAHA2QADAMAHHASWDAWXN84SDAA11 +# If currency does not match [TALER] section, the auditor +# will be ignored! +CURRENCY = EUR + +# Where do we store the auditor's private key? +AUDITOR_PRIV_FILE = ${TALER_DATA_HOME}/auditor/offline-keys/auditor.priv + +# Auditors must be in sections "auditor-", the rest of the section +# name could be anything. +[auditor-ezb] +# Informal name of the auditor. Just for the user. +NAME = European Central Bank + +# URL of the auditor (especially for in the future, when the +# auditor offers an automated issue reporting system). +# Not really used today. +URL = http://taler.ezb.eu/ + +# This is the important bit: the signing key of the auditor. +PUBLIC_KEY = 9QXF7XY7E9VPV47B5Z806NDFSX2VJ79SVHHD29QEQ3BG31ANHZ60 + +# Which currency is this auditor trusted for? +CURRENCY = EUR + + +################################################### +# Configuration for the exchange for the testcase # +################################################### + +[exchange] +# How to access our database +DB = postgres + +# HTTP port the exchange listens to +PORT = 8081 + +# how long are the signatures with the signkey valid? +SIGNKEY_LEGAL_DURATION = 2 years + +# Our public key +MASTER_PUBLIC_KEY = T1VVFQZZARQ1CMF4BN58EE7SKTW5AV2BS18S87ZEGYS4S29J6DNG + +# Base URL of the exchange. +BASE_URL = "http://localhost:8081/" +#BASE_URL = https://exchange.test.taler.net/ + +KEYDIR = ${TALER_DATA_HOME}/exchange/live-keys/ + +REVOCATION_DIR = ${TALER_DATA_HOME}/exchange/revocations/ + + +# Network configuration for the normal API/service HTTP server +# serve via tcp socket (on PORT) +SERVE = tcp + +[exchange-offline] + +# Where do we store the offline master private key of the exchange? +MASTER_PRIV_FILE = ${TALER_DATA_HOME}/exchange/offline-keys/master.priv + +# Where do we store the TOFU key material? +SECM_TOFU_FILE = ${TALER_DATA_HOME}/exchange/offline-keys/secm_tofus.pub + + +[exchangedb-postgres] +CONFIG = "postgres:///talercheck" + +[auditordb-postgres] +CONFIG = "postgres:///talercheck" + +# Account of the EXCHANGE +[exchange-account-exchange] +# What is the exchange's bank account (with the "Taler Bank" demo system)? +PAYTO_URI = "payto://x-taler-bank/localhost:8082/2" + +WIRE_GATEWAY_URL = "http://localhost:8082/2/" +WIRE_GATEWAY_AUTH_METHOD = NONE + +ENABLE_DEBIT = YES +ENABLE_CREDIT = YES + + +[coin_eur_ct_1] +value = EUR:0.01 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.00 +fee_deposit = EUR:0.00 +fee_refresh = EUR:0.01 +fee_refund = EUR:0.01 +rsa_keysize = 1024 + +[coin_eur_ct_10] +value = EUR:0.10 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +fee_refund = EUR:0.01 +rsa_keysize = 1024 + +[coin_eur_1] +value = EUR:1 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +fee_refund = EUR:0.01 +rsa_keysize = 1024 + +[coin_eur_5] +value = EUR:5 +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +fee_refund = EUR:0.01 +rsa_keysize = 1024 diff --git a/src/testing/test_anastasis_api_home/.config/taler/exchange/account-2.json b/src/testing/test_anastasis_api_home/.config/taler/exchange/account-2.json new file mode 100644 index 0000000..f798275 --- /dev/null +++ b/src/testing/test_anastasis_api_home/.config/taler/exchange/account-2.json @@ -0,0 +1,3 @@ +{ + "payto_uri": "payto://x-taler-bank/localhost:8082/2", + "master_sig": "AM32QB4RYMWK548PE63PJXJMWSA001TFFWTZZPSSD8HQ8JE4D5V5X8WTSYSX59ANF4YRTRMF5Q4Q12CE2KTA8KQ03CM11YDTK75SJ20"} diff --git a/src/testing/test_anastasis_api_home/.config/taler/merchant/account-3.json b/src/testing/test_anastasis_api_home/.config/taler/merchant/account-3.json new file mode 100644 index 0000000..016e133 --- /dev/null +++ b/src/testing/test_anastasis_api_home/.config/taler/merchant/account-3.json @@ -0,0 +1 @@ +{"salt":"ZSHKZ4DRFMFRFBMB10BX8M1GD16AZNG639AY8QMW26XBGECQRS1H7H8CQ4WETHT66PG99CYBVGFRKJ7ZENEDJJ4E0X0JHRY1QKPQ3W0","payto_uri":"payto://x-taler-bank/localhost:8082/3"} diff --git a/src/testing/test_anastasis_api_home/.config/taler/merchant/default.priv b/src/testing/test_anastasis_api_home/.config/taler/merchant/default.priv new file mode 100644 index 0000000..3ed8c04 --- /dev/null +++ b/src/testing/test_anastasis_api_home/.config/taler/merchant/default.priv @@ -0,0 +1 @@ + +*/ +/** + * @file lib/testing_api_cmd_config.c + * @brief command to obtain the configuration of an anastasis backend service. + * @author Dennis Neufeld + * @author Dominik Meister + */ + +#include "platform.h" +#include "anastasis_testing_lib.h" +#include +#include + + +/** + * State for a "config" CMD. + */ +struct ConfigState +{ + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * URL of the anastasis backend. + */ + const char *anastasis_url; + + /** + * Expected status code. + */ + unsigned int http_status; + + /** + * The /config GET operation handle. + */ + struct ANASTASIS_ConfigOperation *so; + + /** + * The salt value from server. + */ + struct ANASTASIS_CRYPTO_ProviderSaltP salt; +}; + + +/** + * Function called with the results of a #ANASTASIS_config(). + * + * @param cls closure + * @param http_status HTTP status of the request + * @param config config from the server + */ +static void +config_cb (void *cls, + unsigned int http_status, + const struct ANASTASIS_Config *config) +{ + struct ConfigState *ss = cls; + + ss->so = NULL; + if (http_status != ss->http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to command %s in %s:%u\n", + http_status, + ss->is->commands[ss->is->ip].label, + __FILE__, + __LINE__); + TALER_TESTING_interpreter_fail (ss->is); + return; + } + if (NULL == config) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Config is NULL, command %s in %s:%u\n", + ss->is->commands[ss->is->ip].label, + __FILE__, + __LINE__); + TALER_TESTING_interpreter_fail (ss->is); + return; + } + ss->salt = config->salt; + TALER_TESTING_interpreter_next (ss->is); +} + + +/** + * Run a "config" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +config_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct ConfigState *ss = cls; + + ss->is = is; + ss->so = ANASTASIS_get_config (is->ctx, + ss->anastasis_url, + &config_cb, + ss); + if (NULL == ss->so) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ss->is); + return; + } +} + + +/** + * Free the state of a "config" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +config_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct ConfigState *ss = cls; + + if (NULL != ss->so) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (config)\n", + cmd->label); + ANASTASIS_config_cancel (ss->so); + ss->so = NULL; + } + GNUNET_free (ss); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param ret[out] result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static int +config_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct ConfigState *ss = cls; + + struct TALER_TESTING_Trait traits[] = { + ANASTASIS_TESTING_make_trait_salt (0, + &ss->salt), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_config (const char *label, + const char *anastasis_url, + unsigned int http_status) +{ + struct ConfigState *ss; + + ss = GNUNET_new (struct ConfigState); + ss->http_status = http_status; + ss->anastasis_url = anastasis_url; + { + struct TALER_TESTING_Command cmd = { + .cls = ss, + .label = label, + .run = &config_run, + .cleanup = &config_cleanup, + .traits = &config_traits + }; + + return cmd; + } +} diff --git a/src/testing/testing_api_cmd_keyshare_lookup.c b/src/testing/testing_api_cmd_keyshare_lookup.c new file mode 100644 index 0000000..895d321 --- /dev/null +++ b/src/testing/testing_api_cmd_keyshare_lookup.c @@ -0,0 +1,465 @@ +/* + 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 +*/ +/** + * @file lib/anastasis_api_keyshare_lookup.c + * @brief Testing of Implementation of the /truth GET + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ + +#include "platform.h" +#include "anastasis_testing_lib.h" +#include +#include +#include + + +/** + * State for a "keyshare lookup" CMD. + */ +struct KeyShareLookupState +{ + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * URL of the anastasis backend. + */ + const char *anastasis_url; + + /** + * Expected status code. + */ + enum ANASTASIS_KeyShareDownloadStatus expected_ksdd; + + /** + * The /truth GET operation handle. + */ + struct ANASTASIS_KeyShareLookupOperation *kslo; + + /** + * answer to a challenge + */ + const char *answer; + + /** + * Reference to upload command we expect to lookup. + */ + const char *upload_reference; + + /** + * Reference to upload command we expect to lookup. + */ + const char *payment_reference; + + /** + * Payment secret requested by the service, if any. + */ + struct ANASTASIS_PaymentSecretP payment_secret_response; + + /** + * Taler-URI with payment request, if any. + */ + char *pay_uri; + + /** + * Order ID for payment request, if any. + */ + char *order_id; + + /** + * Redirect-URI for challenge, if any. + */ + char *redirect_uri; + + /** + * "code" returned by service, if any. + */ + char *code; + + /** + * "instructions" for how to solve the challenge as returned by service, if any. + */ + char *instructions; + + /** + * Name of the file where the service will write the challenge, if method is "file". + * Otherwise NULL. + */ + char *filename; + + /** + * Mode for the lookup(0 = question, 1 = code based) + */ + int lookup_mode; + +}; + + +static void +keyshare_lookup_cb (void *cls, + const struct ANASTASIS_KeyShareDownloadDetails *dd) +{ + struct KeyShareLookupState *ksls = cls; + + ksls->kslo = NULL; + if (dd->status != ksls->expected_ksdd) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to command %s in %s:%u\n", + dd->status, + ksls->is->commands[ksls->is->ip].label, + __FILE__, + __LINE__); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + switch (dd->status) + { + case ANASTASIS_KSD_SUCCESS: + break; + case ANASTASIS_KSD_PAYMENT_REQUIRED: + ksls->pay_uri = GNUNET_strdup (dd->details.payment_required.taler_pay_uri); + ksls->payment_secret_response = dd->details.payment_required.payment_secret; + { + struct TALER_MERCHANT_PayUriData pd; + + if (GNUNET_OK != + TALER_MERCHANT_parse_pay_uri (ksls->pay_uri, + &pd)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + ksls->order_id = GNUNET_strdup (pd.order_id); + TALER_MERCHANT_parse_pay_uri_free (&pd); + } + + break; + case ANASTASIS_KSD_INVALID_ANSWER: + if (ksls->filename) + { + FILE *file; + char code[22]; + + file = fopen (ksls->filename, + "r"); + if (NULL == file) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + ksls->filename); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + if (0 == fscanf (file, + "%21s", + code)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "fscanf", + ksls->filename); + GNUNET_break (0 == fclose (file)); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + GNUNET_break (0 == fclose (file)); + ksls->code = GNUNET_strdup (code); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Read code `%s'\n", + code); + } + else + { + ksls->instructions = GNUNET_strndup ( + dd->details.open_challenge.body, + dd->details.open_challenge.body_size); + } + break; + case ANASTASIS_KSD_REDIRECT_FOR_AUTHENTICATION: + ksls->redirect_uri = GNUNET_strdup (dd->details.redirect_url); + break; + case ANASTASIS_KSD_SERVER_ERROR: + break; + case ANASTASIS_KSD_CLIENT_FAILURE: + break; + case ANASTASIS_KSD_TRUTH_UNKNOWN: + break; + case ANASTASIS_KSD_RATE_LIMIT_EXCEEDED: + break; + } + TALER_TESTING_interpreter_next (ksls->is); +} + + +static void +keyshare_lookup_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct KeyShareLookupState *ksls = cls; + const struct ANASTASIS_CRYPTO_TruthKeyP *truth_key; + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid; + const struct ANASTASIS_PaymentSecretP *payment_secret; + const char *answer; + + ksls->is = is; + if (NULL == ksls->upload_reference) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + { + const struct TALER_TESTING_Command *upload_cmd; + + upload_cmd = TALER_TESTING_interpreter_lookup_command ( + is, + ksls->upload_reference); + if (NULL == upload_cmd) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + { + const char *fn; + + if (GNUNET_OK != + TALER_TESTING_get_trait_string (upload_cmd, + 0, + &fn)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + if (NULL != fn) + ksls->filename = GNUNET_strdup (fn); + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_truth_uuid (upload_cmd, + 0, + &truth_uuid)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + if (NULL == truth_uuid) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_truth_key (upload_cmd, + 0, + &truth_key)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + if (NULL == truth_key) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + } + + if (ksls->lookup_mode == 1) + { + const struct TALER_TESTING_Command *download_cmd; + + download_cmd = TALER_TESTING_interpreter_lookup_command (is, + ksls->answer); + if (NULL == download_cmd) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_code (download_cmd, + 0, + &answer)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + if (NULL == answer) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + } + else + { + /* answer is the answer */ + answer = ksls->answer; + } + + if (NULL != ksls->payment_reference) + { + const struct TALER_TESTING_Command *payment_cmd; + + payment_cmd = TALER_TESTING_interpreter_lookup_command + (is, + ksls->payment_reference); + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_payment_secret (payment_cmd, + 0, + &payment_secret)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } + } + else + { + payment_secret = NULL; + } + + { + struct GNUNET_HashCode h_answer; + + if (NULL != answer) + GNUNET_CRYPTO_hash (answer, + strlen (answer), + &h_answer); + ksls->kslo = ANASTASIS_keyshare_lookup (is->ctx, + ksls->anastasis_url, + truth_uuid, + truth_key, + payment_secret, + GNUNET_TIME_UNIT_ZERO, + (NULL != answer) + ? &h_answer + : NULL, + &keyshare_lookup_cb, + ksls); + } + if (NULL == ksls->kslo) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ksls->is); + return; + } +} + + +static void +keyshare_lookup_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct KeyShareLookupState *ksls = cls; + + if (NULL != ksls->kslo) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (keyshare lookup)\n", + cmd->label); + ANASTASIS_keyshare_lookup_cancel (ksls->kslo); + ksls->kslo = NULL; + } + GNUNET_free (ksls->pay_uri); + GNUNET_free (ksls->order_id); + GNUNET_free (ksls->code); + GNUNET_free (ksls->instructions); + GNUNET_free (ksls->redirect_uri); + GNUNET_free (ksls); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param[out] ret result (could be anything) + * @param[out] trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static int +keyshare_lookup_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct KeyShareLookupState *ksls = cls; + struct TALER_TESTING_Trait traits[] = { + ANASTASIS_TESTING_make_trait_payment_secret (0, + &ksls->payment_secret_response), + TALER_TESTING_make_trait_url (TALER_TESTING_UT_TALER_URL, + ksls->pay_uri), + TALER_TESTING_make_trait_order_id (0, + ksls->order_id), + ANASTASIS_TESTING_make_trait_code (0, + ksls->code), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +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) +{ + struct KeyShareLookupState *ksls; + + GNUNET_assert (NULL != upload_ref); + ksls = GNUNET_new (struct KeyShareLookupState); + ksls->expected_ksdd = ksdd; + ksls->anastasis_url = anastasis_url; + ksls->upload_reference = upload_ref; + ksls->payment_reference = payment_ref; + ksls->answer = answer; + ksls->lookup_mode = lookup_mode; + { + struct TALER_TESTING_Command cmd = { + .cls = ksls, + .label = label, + .run = &keyshare_lookup_run, + .cleanup = &keyshare_lookup_cleanup, + .traits = &keyshare_lookup_traits + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_keyshare_lookup.c */ diff --git a/src/testing/testing_api_cmd_policy_lookup.c b/src/testing/testing_api_cmd_policy_lookup.c new file mode 100644 index 0000000..e97f746 --- /dev/null +++ b/src/testing/testing_api_cmd_policy_lookup.c @@ -0,0 +1,267 @@ +/* + 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 +*/ +/** + * @file lib/testing_api_cmd_policy_lookup.c + * @brief command to execute the anastasis backend service. + * @author Dennis Neufeld + * @author Dominik Meister + */ + +#include "platform.h" +#include "anastasis_testing_lib.h" +#include +#include + + +/** + * State for a "policy lookup" CMD. + */ +struct PolicyLookupState +{ + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Eddsa Publickey. + */ + struct ANASTASIS_CRYPTO_AccountPublicKeyP anastasis_pub; + + /** + * Hash of the upload (all zeros if there was no upload). + */ + const struct GNUNET_HashCode *upload_hash; + + /** + * URL of the anastasis backend. + */ + const char *anastasis_url; + + /** + * Expected status code. + */ + unsigned int http_status; + + /** + * Reference to upload command we expect to lookup. + */ + const char *upload_reference; + + /** + * The /policy GET operation handle. + */ + struct ANASTASIS_PolicyLookupOperation *plo; +}; + + +/** + * Function called with the results of a #ANASTASIS_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 PolicyLookupState *pls = cls; + + pls->plo = NULL; + if (http_status != pls->http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to command %s in %s:%u\n", + http_status, + pls->is->commands[pls->is->ip].label, + __FILE__, + __LINE__); + TALER_TESTING_interpreter_fail (pls->is); + return; + } + if (NULL != pls->upload_reference) + { + if ( (MHD_HTTP_OK == http_status) && + (0 != GNUNET_memcmp (&dd->curr_policy_hash, + pls->upload_hash)) ) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pls->is); + return; + } + } + TALER_TESTING_interpreter_next (pls->is); +} + + +/** + * Run a "policy lookup" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +policy_lookup_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct PolicyLookupState *pls = cls; + + pls->is = is; + if (NULL != pls->upload_reference) + { + const struct TALER_TESTING_Command *upload_cmd; + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *anastasis_pub; + + upload_cmd = TALER_TESTING_interpreter_lookup_command + (is, + pls->upload_reference); + if (NULL == upload_cmd) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pls->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_hash (upload_cmd, + ANASTASIS_TESTING_TRAIT_HASH_CURRENT, + &pls->upload_hash)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pls->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_account_pub (upload_cmd, + 0, + &anastasis_pub)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pls->is); + return; + } + pls->anastasis_pub = *anastasis_pub; + } + pls->plo = ANASTASIS_policy_lookup (is->ctx, + pls->anastasis_url, + &pls->anastasis_pub, + &policy_lookup_cb, + pls); + if (NULL == pls->plo) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pls->is); + return; + } +} + + +/** + * Free the state of a "policy lookup" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +policy_lookup_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct PolicyLookupState *pls = cls; + + if (NULL != pls->plo) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (policy lookup)\n", + cmd->label); + ANASTASIS_policy_lookup_cancel (pls->plo); + pls->plo = NULL; + } + GNUNET_free (pls); +} + + +/** + * Make the "policy lookup" command. + * + * @param label command label + * @param anastasis_url base URL of the ANASTASIS serving + * the policy store 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) +{ + struct PolicyLookupState *pls; + + GNUNET_assert (NULL != upload_ref); + pls = GNUNET_new (struct PolicyLookupState); + pls->http_status = http_status; + pls->anastasis_url = anastasis_url; + pls->upload_reference = upload_ref; + { + struct TALER_TESTING_Command cmd = { + .cls = pls, + .label = label, + .run = &policy_lookup_run, + .cleanup = &policy_lookup_cleanup + }; + + return cmd; + } +} + + +/** + * Make the "policy lookup" command for a non-existent upload. + * + * @param label command label + * @param anastasis_url base URL of the ANASTASIS serving + * the policy lookup request. + * @return the command + */ +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_policy_nx (const char *label, + const char *anastasis_url) +{ + struct PolicyLookupState *pls; + struct GNUNET_CRYPTO_EddsaPrivateKey priv; + + pls = GNUNET_new (struct PolicyLookupState); + pls->http_status = MHD_HTTP_NOT_FOUND; + pls->anastasis_url = anastasis_url; + GNUNET_CRYPTO_eddsa_key_create (&priv); + GNUNET_CRYPTO_eddsa_key_get_public (&priv, + &pls->anastasis_pub.pub); + { + struct TALER_TESTING_Command cmd = { + .cls = pls, + .label = label, + .run = &policy_lookup_run, + .cleanup = &policy_lookup_cleanup + }; + + return cmd; + } +} diff --git a/src/testing/testing_api_cmd_policy_store.c b/src/testing/testing_api_cmd_policy_store.c new file mode 100644 index 0000000..a8f0a70 --- /dev/null +++ b/src/testing/testing_api_cmd_policy_store.c @@ -0,0 +1,397 @@ +/* + This file is part of ANASTASIS + Copyright (C) 2014-2019 Taler Systems SA + + ANASTASIS 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. + + 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. If not, see + +*/ + +/** + * @file lib/testing_api_cmd_policy_store.c + * @brief command to execute the anastasis backend service. + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ + +#include "platform.h" +#include "anastasis_testing_lib.h" +#include +#include +#include + + +/** + * State for a "policy store" CMD. + */ +struct PolicyStoreState +{ + /** + * Claim token we got back, if any. Otherwise all zeros. + */ + struct TALER_ClaimTokenP claim_token; + + /** + * The policy data. + */ + const void *recovery_data; + + /** + * Number of bytes in @e recovery_data + */ + size_t recovery_data_size; + + /** + * Expected status code. + */ + unsigned int http_status; + + /** + * Eddsa Publickey. + */ + struct ANASTASIS_CRYPTO_AccountPublicKeyP anastasis_pub; + + /** + * Eddsa Privatekey. + */ + struct ANASTASIS_CRYPTO_AccountPrivateKeyP anastasis_priv; + + /** + * Hash of uploaded data, used to verify the response. + */ + struct GNUNET_HashCode curr_hash; + + /** + * The /policy POST operation handle. + */ + struct ANASTASIS_PolicyStoreOperation *pso; + + /** + * The nonce. + */ + struct ANASTASIS_CRYPTO_NonceP nonce; + + /** + * URL of the anastasis backend. + */ + const char *anastasis_url; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Previous upload, or NULL for none. Used to calculate what THIS + * upload is based on. + */ + const char *prev_upload; + + /** + * Payment order ID we are to provide in the request, or zero. + */ + struct ANASTASIS_PaymentSecretP payment_secret_request; + + /** + * The order ID, for making the payment. + */ + char *order_id; + + /** + * Payment order ID we are to provide in the response, or zero. + */ + struct ANASTASIS_PaymentSecretP payment_secret_response; + + /** + * Options for how we are supposed to do the upload. + */ + enum ANASTASIS_TESTING_PolicyStoreOption psopt; + + /** + * True if @e payment_secret_request is initialized. + */ + bool payment_secret_set; +}; + +/** + * Function called with the results of a #policy_store(). + * + * @param cls closure + * @param http_status HTTP status of the request + * @param ud details about the upload operation + */ +static void +policy_store_cb (void *cls, + const struct ANASTASIS_UploadDetails *ud) +{ + struct PolicyStoreState *pss = cls; + + pss->pso = NULL; + if (ud->http_status != pss->http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to command %s in %s:%u\n", + ud->http_status, + pss->is->commands[pss->is->ip].label, + __FILE__, + __LINE__); + TALER_TESTING_interpreter_fail (pss->is); + return; + } + switch (ud->us) + { + case ANASTASIS_US_SUCCESS: + if (0 != GNUNET_memcmp (&pss->curr_hash, + ud->details.success.curr_backup_hash)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pss->is); + return; + } + break; + case ANASTASIS_US_PAYMENT_REQUIRED: + pss->payment_secret_response = ud->details.payment.ps; + { + struct TALER_MERCHANT_PayUriData pd; + + if (GNUNET_OK != + TALER_MERCHANT_parse_pay_uri (ud->details.payment.payment_request, + &pd)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pss->is); + return; + } + pss->order_id = GNUNET_strdup (pd.order_id); + if (NULL != pd.claim_token) + pss->claim_token = *pd.claim_token; + TALER_MERCHANT_parse_pay_uri_free (&pd); + } + break; + case ANASTASIS_US_HTTP_ERROR: + break; + case ANASTASIS_US_CLIENT_ERROR: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pss->is); + return; + case ANASTASIS_US_SERVER_ERROR: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pss->is); + return; + default: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pss->is); + return; + } + TALER_TESTING_interpreter_next (pss->is); +} + + +/** + * Run a "policy store" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +policy_store_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct PolicyStoreState *pss = cls; + + pss->is = is; + if (NULL != pss->prev_upload) + { + const struct TALER_TESTING_Command *ref; + + ref = TALER_TESTING_interpreter_lookup_command (is, + pss->prev_upload); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pss->is); + return; + } + { + const struct ANASTASIS_CRYPTO_AccountPrivateKeyP *priv; + + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_account_priv (ref, + 0, + &priv)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pss->is); + return; + } + pss->anastasis_priv = *priv; + } + { + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *pub; + + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_account_pub (ref, + 0, + &pub)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pss->is); + return; + } + pss->anastasis_pub = *pub; + } + { + const struct ANASTASIS_PaymentSecretP *ps; + + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_payment_secret (ref, + 0, + &ps)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pss->is); + return; + } + pss->payment_secret_request = *ps; + pss->payment_secret_set = true; + } + } + else + { + GNUNET_CRYPTO_eddsa_key_create (&pss->anastasis_priv.priv); + GNUNET_CRYPTO_eddsa_key_get_public (&pss->anastasis_priv.priv, + &pss->anastasis_pub.pub); + } + + GNUNET_CRYPTO_hash (pss->recovery_data, + pss->recovery_data_size, + &pss->curr_hash); + pss->pso = ANASTASIS_policy_store ( + is->ctx, + pss->anastasis_url, + &pss->anastasis_priv, + pss->recovery_data, + pss->recovery_data_size, + (0 != (ANASTASIS_TESTING_PSO_REQUEST_PAYMENT & pss->psopt)), + pss->payment_secret_set ? &pss->payment_secret_request : NULL, + GNUNET_TIME_UNIT_ZERO, + &policy_store_cb, + pss); + if (NULL == pss->pso) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pss->is); + return; + } +} + + +/** + * Free the state of a "policy store" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +policy_store_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct PolicyStoreState *pss = cls; + + if (NULL != pss->pso) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (policy post)\n", + cmd->label); + ANASTASIS_policy_store_cancel (pss->pso); + pss->pso = NULL; + } + GNUNET_free (pss->order_id); + GNUNET_free (pss); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param[out] ret result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static int +policy_store_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct PolicyStoreState *pss = cls; + struct TALER_TESTING_Trait traits[] = { + TALER_TESTING_make_trait_claim_token (0, + &pss->claim_token), + TALER_TESTING_make_trait_order_id (0, + pss->order_id), + ANASTASIS_TESTING_make_trait_hash (0, + &pss->curr_hash), + ANASTASIS_TESTING_make_trait_account_pub (0, + &pss->anastasis_pub), + ANASTASIS_TESTING_make_trait_account_priv (0, + &pss->anastasis_priv), + ANASTASIS_TESTING_make_trait_payment_secret (0, + &pss->payment_secret_response), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +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) +{ + struct PolicyStoreState *pss; + + pss = GNUNET_new (struct PolicyStoreState); + pss->recovery_data = recovery_data; + pss->recovery_data_size = recovery_data_size; + pss->http_status = http_status; + pss->psopt = pso; + pss->anastasis_url = anastasis_url; + pss->prev_upload = prev_upload; + { + struct TALER_TESTING_Command cmd = { + .cls = pss, + .label = label, + .run = &policy_store_run, + .cleanup = &policy_store_cleanup, + .traits = &policy_store_traits + }; + + return cmd; + } +} diff --git a/src/testing/testing_api_cmd_truth_store.c b/src/testing/testing_api_cmd_truth_store.c new file mode 100644 index 0000000..0883406 --- /dev/null +++ b/src/testing/testing_api_cmd_truth_store.c @@ -0,0 +1,436 @@ +/* + 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 +*/ +/** + * @file lib/testing_api_cmd_truth_store.c + * @brief command to execute the anastasis backend service. + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis_testing_lib.h" +#include +#include +#include + +/** + * State for a "truth store" CMD. + */ +struct TruthStoreState +{ + /** + * UUID of the uploaded truth + */ + struct ANASTASIS_CRYPTO_TruthUUIDP uuid; + + /** + * Key used to encrypt the @e truth_data on the server. + */ + struct ANASTASIS_CRYPTO_TruthKeyP key; + + /** + * "Encrypted" key share data we store at the server. + */ + struct ANASTASIS_CRYPTO_EncryptedKeyShareP encrypted_keyshare; + + /** + * The /truth POST operation handle. + */ + struct ANASTASIS_TruthStoreOperation *tso; + + /** + * URL of the anastasis backend. + */ + const char *anastasis_url; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Previous upload, or NULL for none. Used to calculate what THIS + * upload is based on. + */ + const char *prev_upload; + + /** + * Authorization method / plugin name. + */ + const char *method; + + /** + * Mimetype of @e truth_data. + */ + const char *mime_type; + + /** + * Number of bytes in @e truth_data + */ + size_t truth_data_size; + + /** + * Data used by the authorization process. + */ + void *truth_data; + + /** + * Name of the file where the service will write the challenge, or NULL. + */ + char *filename; + + /** + * Expected status code. + */ + unsigned int http_status; + + /** + * Payment request we got back, or NULL. + */ + char *pay_uri; + + /** + * Payment order ID we got back, or all zeros. + */ + struct ANASTASIS_PaymentSecretP payment_secret_response; + + /** + * Options for how we are supposed to do the upload. + */ + enum ANASTASIS_TESTING_TruthStoreOption tsopt; +}; + +/** + * Function called with the results of a #truth_store(). + * + * @param cls closure + * @param ec ANASTASIS error code + * @param http_status HTTP status of the request + * @param ud details about the upload operation + */ +static void +truth_store_cb (void *cls, + const struct ANASTASIS_UploadDetails *ud) +{ + struct TruthStoreState *tss = cls; + + tss->tso = NULL; + if ( (NULL == ud) || + (ud->http_status != tss->http_status) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to command %s in %s:%u\n", + (NULL != ud) ? ud->http_status : 0, + tss->is->commands[tss->is->ip].label, + __FILE__, + __LINE__); + TALER_TESTING_interpreter_fail (tss->is); + return; + } + switch (ud->us) + { + case ANASTASIS_US_SUCCESS: + break; + case ANASTASIS_US_PAYMENT_REQUIRED: + tss->pay_uri = GNUNET_strdup (ud->details.payment.payment_request); + tss->payment_secret_response = ud->details.payment.ps; + break; + case ANASTASIS_US_CONFLICTING_TRUTH: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + case ANASTASIS_US_HTTP_ERROR: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + case ANASTASIS_US_CLIENT_ERROR: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + case ANASTASIS_US_SERVER_ERROR: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + default: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + } + TALER_TESTING_interpreter_next (tss->is); +} + + +/** + * Run a "truth store" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +truth_store_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct TruthStoreState *tss = cls; + + tss->is = is; + if (NULL != tss->prev_upload) + { + const struct TALER_TESTING_Command *ref; + + ref = TALER_TESTING_interpreter_lookup_command (is, + tss->prev_upload); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + } + + if (0 != (ANASTASIS_TESTING_TSO_REFERENCE_UUID & tss->tsopt)) + { + const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid; + const struct ANASTASIS_CRYPTO_EncryptedKeyShareP *eks; + + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_truth_uuid (ref, + 0, + &uuid)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + } + tss->uuid = *uuid; + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_eks (ref, + 0, + &eks)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + } + tss->encrypted_keyshare = *eks; + } + } + else + { + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &tss->uuid, + sizeof (struct ANASTASIS_CRYPTO_TruthUUIDP)); + GNUNET_CRYPTO_random_block ( + GNUNET_CRYPTO_QUALITY_WEAK, + &tss->encrypted_keyshare, + sizeof (struct ANASTASIS_CRYPTO_EncryptedKeyShareP)); + } + GNUNET_CRYPTO_random_block ( + GNUNET_CRYPTO_QUALITY_WEAK, + &tss->key, + sizeof (struct ANASTASIS_CRYPTO_TruthKeyP)); + + { + void *encrypted_truth; + size_t size_encrypted_truth; + struct ANASTASIS_CRYPTO_NonceP nonce; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &nonce, + sizeof (nonce)); + ANASTASIS_CRYPTO_truth_encrypt (&nonce, + &tss->key, + tss->truth_data, + tss->truth_data_size, + &encrypted_truth, + &size_encrypted_truth); + { + void *t; + size_t t_size; + + ANASTASIS_CRYPTO_truth_decrypt (&tss->key, + encrypted_truth, + size_encrypted_truth, + &t, + &t_size); + if ( (t_size != tss->truth_data_size) || + (0 != memcmp (tss->truth_data, + t, + t_size)) ) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + } + GNUNET_free (t); + } + tss->tso = ANASTASIS_truth_store ( + is->ctx, + tss->anastasis_url, + &tss->uuid, + tss->method, + &tss->encrypted_keyshare, + tss->mime_type, + size_encrypted_truth, + encrypted_truth, + (0 != (ANASTASIS_TESTING_TSO_REQUEST_PAYMENT & tss->tsopt)), + GNUNET_TIME_UNIT_ZERO, + &truth_store_cb, + tss); + GNUNET_free (encrypted_truth); + } + if (NULL == tss->tso) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tss->is); + return; + } +} + + +/** + * Free the state of a "truth store" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +truth_store_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct TruthStoreState *tss = cls; + + if (NULL != tss->tso) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (truth post)\n", + cmd->label); + ANASTASIS_truth_store_cancel (tss->tso); + tss->tso = NULL; + } + GNUNET_free (tss->truth_data); + GNUNET_free (tss->pay_uri); + GNUNET_free (tss->filename); + GNUNET_free (tss); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param[out] ret result (could be anything) + * @param[out] trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static int +truth_store_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct TruthStoreState *tss = cls; + struct TALER_TESTING_Trait traits[] = { + ANASTASIS_TESTING_make_trait_truth_uuid (0, + &tss->uuid), + ANASTASIS_TESTING_make_trait_truth_key (0, + &tss->key), + ANASTASIS_TESTING_make_trait_eks (0, + &tss->encrypted_keyshare), + ANASTASIS_TESTING_make_trait_payment_secret (0, + &tss->payment_secret_response), + TALER_TESTING_make_trait_url (TALER_TESTING_UT_TALER_URL, + tss->pay_uri), + TALER_TESTING_make_trait_string (0, + tss->filename), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +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) +{ + struct TruthStoreState *tss; + + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Storing %u bytes of truth\n", + (unsigned int) truth_data_size); + tss = GNUNET_new (struct TruthStoreState); + tss->http_status = http_status; + tss->tsopt = tso; + tss->anastasis_url = anastasis_url; + tss->prev_upload = prev_upload; + tss->method = method; + tss->mime_type = mime_type; + tss->truth_data = GNUNET_memdup (truth_data, + truth_data_size); + tss->truth_data_size = truth_data_size; + if (0 == strcasecmp (method, + "file")) + tss->filename = GNUNET_strndup (truth_data, + truth_data_size); + { + struct TALER_TESTING_Command cmd = { + .cls = tss, + .label = label, + .run = &truth_store_run, + .cleanup = &truth_store_cleanup, + .traits = &truth_store_traits + }; + + return cmd; + } +} + + +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) +{ + struct GNUNET_HashCode h; + + GNUNET_CRYPTO_hash (answer, + strlen (answer), + &h); + return ANASTASIS_TESTING_cmd_truth_store (label, + anastasis_url, + prev_upload, + "question", + "binary/sha512", + sizeof (h), + &h, + tso, + http_status); +} diff --git a/src/testing/testing_api_helpers.c b/src/testing/testing_api_helpers.c new file mode 100644 index 0000000..66e7032 --- /dev/null +++ b/src/testing/testing_api_helpers.c @@ -0,0 +1,173 @@ +/* + This file is part of ANASTASIS + Copyright (C) 2014-2021 Taler Systems SA + + ANASTASIS 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. + + ANASTASIS is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + ANASTASISABILITY 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. If not, see + +*/ + +/** + * @file lib/testing_api_helpers.c + * @brief helper functions for test library. + * @author Christian Grothoff + * @author Marcello Stanisci + */ + +#include "platform.h" +#include "anastasis_testing_lib.h" +#include + + +struct GNUNET_OS_Process * +ANASTASIS_TESTING_run_anastasis (const char *config_filename, + const char *anastasis_url) +{ + struct GNUNET_OS_Process *anastasis_proc; + unsigned int iter; + char *wget_cmd; + + anastasis_proc + = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ALL, + NULL, NULL, NULL, + "anastasis-httpd", + "anastasis-httpd", + "--log=INFO", + "-c", config_filename, + NULL); + if (NULL == anastasis_proc) + ANASTASIS_FAIL (); + + GNUNET_asprintf (&wget_cmd, + "wget -q -t 1 -T 1" + " %s" + " -o /dev/null -O /dev/null", + anastasis_url); + + /* give child time to start and bind against the socket */ + fprintf (stderr, + "Waiting for `anastasis-httpd' to be ready\n"); + iter = 0; + do + { + if (100 == iter) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to launch `anastasis-httpd' (or `wget')\n"); + GNUNET_OS_process_kill (anastasis_proc, + SIGTERM); + GNUNET_OS_process_wait (anastasis_proc); + GNUNET_OS_process_destroy (anastasis_proc); + ANASTASIS_FAIL (); + } + { + struct timespec req = { + .tv_nsec = 10000 + }; + + nanosleep (&req, + NULL); + } + iter++; + } + while (0 != system (wget_cmd)); + GNUNET_free (wget_cmd); + fprintf (stderr, + "\n"); + return anastasis_proc; +} + + +char * +ANASTASIS_TESTING_prepare_anastasis (const char *config_filename) +{ + struct GNUNET_CONFIGURATION_Handle *cfg; + unsigned long long port; + struct GNUNET_OS_Process *dbinit_proc; + enum GNUNET_OS_ProcessStatusType type; + unsigned long code; + char *base_url; + + cfg = GNUNET_CONFIGURATION_create (); + if (GNUNET_OK != + GNUNET_CONFIGURATION_load (cfg, + config_filename)) + ANASTASIS_FAIL (); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (cfg, + "anastasis", + "PORT", + &port)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "PORT"); + GNUNET_CONFIGURATION_destroy (cfg); + return NULL; + } + + GNUNET_CONFIGURATION_destroy (cfg); + + if (GNUNET_OK != + GNUNET_NETWORK_test_port_free (IPPROTO_TCP, + (uint16_t) port)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Required port %llu not available, skipping.\n", + port); + return NULL; + } + + /* DB preparation */ + if (NULL == (dbinit_proc = GNUNET_OS_start_process + (GNUNET_OS_INHERIT_STD_ALL, + NULL, NULL, NULL, + "anastasis-dbinit", + "anastasis-dbinit", + "-c", config_filename, + "-r", + NULL))) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to run anastasis-dbinit. Check your PATH.\n"); + return NULL; + } + + if (GNUNET_SYSERR == + GNUNET_OS_process_wait_status (dbinit_proc, + &type, + &code)) + { + GNUNET_OS_process_destroy (dbinit_proc); + ANASTASIS_FAIL (); + } + if ( (type == GNUNET_OS_PROCESS_EXITED) && + (0 != code) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to setup database\n"); + return NULL; + } + if ( (type != GNUNET_OS_PROCESS_EXITED) || + (0 != code) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected error running `anastasis-dbinit'!\n"); + return NULL; + } + GNUNET_OS_process_destroy (dbinit_proc); + GNUNET_asprintf (&base_url, + "http://localhost:%llu/", + port); + return base_url; +} diff --git a/src/testing/testing_api_trait_account_priv.c b/src/testing/testing_api_trait_account_priv.c new file mode 100644 index 0000000..4860e82 --- /dev/null +++ b/src/testing/testing_api_trait_account_priv.c @@ -0,0 +1,72 @@ +/* + 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 Privlic License for more details. + + You should have received a copy of the GNU General Privlic + License along with TALER; see the file COPYING. If not, see + +*/ +/** + * @file lib/testing_api_trait_account_priv.c + * @brief traits to offer a account_priv + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_ACCOUNT_PRIV "anastasis-account_priv" + + +/** + * Obtain an account private key from @a cmd. + * + * @param cmd command to extract the private key from. + * @param index the private key's index number. + * @param n[out] set to the private key coming from @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) +{ + return cmd->traits (cmd->cls, + (const void **) priv, + ANASTASIS_TESTING_TRAIT_ACCOUNT_PRIV, + index); +} + + +/** + * 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) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_ACCOUNT_PRIV, + .ptr = (const void *) priv + }; + return ret; +} + + +/* end of testing_api_trait_account_priv.c */ diff --git a/src/testing/testing_api_trait_account_pub.c b/src/testing/testing_api_trait_account_pub.c new file mode 100644 index 0000000..5a3632e --- /dev/null +++ b/src/testing/testing_api_trait_account_pub.c @@ -0,0 +1,72 @@ +/* + 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 + +*/ +/** + * @file lib/testing_api_trait_account_pub.c + * @brief traits to offer a account_pub + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_ACCOUNT_PUB "anastasis-account_pub" + + +/** + * Obtain an account public key from @a cmd. + * + * @param cmd command to extract the public key from. + * @param index the public key's index number. + * @param n[out] set to the public key coming from @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) +{ + return cmd->traits (cmd->cls, + (const void **) pub, + ANASTASIS_TESTING_TRAIT_ACCOUNT_PUB, + index); +} + + +/** + * 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) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_ACCOUNT_PUB, + .ptr = (const void *) h + }; + return ret; +} + + +/* end of testing_api_trait_account_pub.c */ diff --git a/src/testing/testing_api_trait_code.c b/src/testing/testing_api_trait_code.c new file mode 100644 index 0000000..1a43cf8 --- /dev/null +++ b/src/testing/testing_api_trait_code.c @@ -0,0 +1,73 @@ +/* + 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 + +*/ +/** + * @file lib/testing_api_trait_string.c + * @brief traits to offers a code for a challenge + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_CODE "anastasis-code" + + +/** + * Obtain a code from @a cmd. + * + * @param cmd command to extract the number from. + * @param index the number's index number. + * @param n[out] 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) +{ + return cmd->traits (cmd->cls, + (const void **) code, + ANASTASIS_TESTING_TRAIT_CODE, + index); +} + + +/** + * Offer a code. + * + * @param index the number's index number. + * @param code the code to offer. + * @return #GNUNET_OK on success. + */ +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_code + (unsigned int index, + const char *code) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_CODE, + .ptr = (const void *) code + }; + return ret; +} + + +/* end of testing_api_trait_string.c */ diff --git a/src/testing/testing_api_trait_eks.c b/src/testing/testing_api_trait_eks.c new file mode 100644 index 0000000..dc3f923 --- /dev/null +++ b/src/testing/testing_api_trait_eks.c @@ -0,0 +1,58 @@ +/* + This file is part of TALER + Copyright (C) 2021 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 + +*/ +/** + * @file lib/testing_api_trait_eks.c + * @brief traits to offer a payment identifier + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_EKS \ + "anastasis-eks" + + +int +ANASTASIS_TESTING_get_trait_eks + (const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_CRYPTO_EncryptedKeyShareP **eks) +{ + return cmd->traits (cmd->cls, + (const void **) eks, + ANASTASIS_TESTING_TRAIT_EKS, + index); +} + + +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_eks + (unsigned int index, + const struct ANASTASIS_CRYPTO_EncryptedKeyShareP *eks) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_EKS, + .ptr = (const void *) eks + }; + return ret; +} + + +/* end of testing_api_trait_eks.c */ diff --git a/src/testing/testing_api_trait_hash.c b/src/testing/testing_api_trait_hash.c new file mode 100644 index 0000000..18be1ea --- /dev/null +++ b/src/testing/testing_api_trait_hash.c @@ -0,0 +1,72 @@ +/* + 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 + +*/ +/** + * @file lib/testing_api_trait_hash.c + * @brief traits to offer a hash + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_HASH "anastasis-hash" + + +/** + * Obtain a hash from @a cmd. + * + * @param cmd command to extract the number from. + * @param index the number's index number. + * @param n[out] set to the number 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) +{ + return cmd->traits (cmd->cls, + (const void **) h, + ANASTASIS_TESTING_TRAIT_HASH, + index); +} + + +/** + * 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) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_HASH, + .ptr = (const void *) h + }; + return ret; +} + + +/* end of testing_api_trait_hash.c */ diff --git a/src/testing/testing_api_trait_payment_secret.c b/src/testing/testing_api_trait_payment_secret.c new file mode 100644 index 0000000..6238879 --- /dev/null +++ b/src/testing/testing_api_trait_payment_secret.c @@ -0,0 +1,73 @@ +/* + 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 + +*/ +/** + * @file lib/testing_api_trait_payment_secret.c + * @brief traits to offer a payment identifier + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_PAYMENT_SECRET \ + "anastasis-payment_secret" + + +/** + * 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 n[out] set to the payment identifier 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) +{ + return cmd->traits (cmd->cls, + (const void **) payment_secret, + ANASTASIS_TESTING_TRAIT_PAYMENT_SECRET, + index); +} + + +/** + * Offer a payment identifier. + * + * @param index usually zero + * @param h the payment identifier 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) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_PAYMENT_SECRET, + .ptr = (const void *) h + }; + return ret; +} + + +/* end of testing_api_trait_payment_secret.c */ diff --git a/src/testing/testing_api_trait_salt.c b/src/testing/testing_api_trait_salt.c new file mode 100644 index 0000000..116742f --- /dev/null +++ b/src/testing/testing_api_trait_salt.c @@ -0,0 +1,74 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 + +*/ +/** + * @file lib/testing_api_trait_salt.c + * @brief traits to offer a hash + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_SALT "anastasis-provider-salt" + + +/** + * Obtain a salt from @a cmd. + * + * @param cmd command to extract the salt from. + * @param index the salt's index number. + * @param s[out] 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) +{ + return cmd->traits (cmd->cls, + (const void **) s, + ANASTASIS_TESTING_TRAIT_SALT, + index); +} + + +/** + * 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) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_SALT, + .ptr = (const void *) s + }; + return ret; +} + + +/* end of testing_api_trait_salt.c */ diff --git a/src/testing/testing_api_trait_truth_key.c b/src/testing/testing_api_trait_truth_key.c new file mode 100644 index 0000000..55094c1 --- /dev/null +++ b/src/testing/testing_api_trait_truth_key.c @@ -0,0 +1,58 @@ +/* + 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 General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, see + +*/ +/** + * @file lib/testing_api_trait_truth_key.c + * @brief traits to offer a payment identifier + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_TRUTH_KEY \ + "anastasis-truth_key" + + +int +ANASTASIS_TESTING_get_trait_truth_key + (const struct TALER_TESTING_Command *cmd, + unsigned int index, + const struct ANASTASIS_CRYPTO_TruthKeyP **truth_key) +{ + return cmd->traits (cmd->cls, + (const void **) truth_key, + ANASTASIS_TESTING_TRAIT_TRUTH_KEY, + index); +} + + +struct TALER_TESTING_Trait +ANASTASIS_TESTING_make_trait_truth_key + (unsigned int index, + const struct ANASTASIS_CRYPTO_TruthKeyP *h) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_TRUTH_KEY, + .ptr = (const void *) h + }; + return ret; +} + + +/* end of testing_api_trait_truth_key.c */ diff --git a/src/testing/testing_api_trait_truth_uuid.c b/src/testing/testing_api_trait_truth_uuid.c new file mode 100644 index 0000000..38a7336 --- /dev/null +++ b/src/testing/testing_api_trait_truth_uuid.c @@ -0,0 +1,74 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 + +*/ +/** + * @file lib/testing_api_trait_truth_pub.c + * @brief traits to offer a UUID for some truth + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_TRUTH_UUID "anastasis-truth-uuid" + + +/** + * Obtain an public key from @a cmd. + * + * @param cmd command to extract the number from. + * @param index the number's index number. + * @param u[out] 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 **tpk) +{ + return cmd->traits (cmd->cls, + (const void **) tpk, + ANASTASIS_TESTING_TRAIT_TRUTH_UUID, + index); +} + + +/** + * Offer a truth public key. + * + * @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_truth_uuid + (unsigned int index, + const struct ANASTASIS_CRYPTO_TruthUUIDP *tpk) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_TRUTH_UUID, + .ptr = (const void *) tpk + }; + return ret; +} + + +/* end of testing_api_trait_truth_pub.c */ diff --git a/src/testing/testing_cmd_challenge_answer.c b/src/testing/testing_cmd_challenge_answer.c new file mode 100644 index 0000000..b243d61 --- /dev/null +++ b/src/testing/testing_cmd_challenge_answer.c @@ -0,0 +1,584 @@ +/* + 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 +*/ +/** + * @file lib/testing_cmd_challenge_answer.c + * @brief command to execute the anastasis recovery service + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ + +#include "platform.h" +#include "anastasis_testing_lib.h" +#include +#include +#include + + +/** + * State for a "challenge answer" CMD. + */ +struct ChallengeState +{ + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Reference to the challenge we are solving + */ + struct ANASTASIS_Challenge *c; + + /** + * Answer to the challenge we are solving + */ + const char *answer; + + /** + * Reference to the recovery process + */ + const char *challenge_ref; + + /** + * Reference to the payment + */ + const char *payment_ref; + + /** + * "taler://pay/" URL we got back, if any. Otherwise NULL. + */ + char *payment_uri; + + /** + * Order ID extracted from @e payment_uri, or NULL. + */ + char *order_id; + + /** + * Payment order ID we are to provide in the request. + */ + struct ANASTASIS_PaymentSecretP payment_order_req; + + /** + * Expected status code. + */ + enum ANASTASIS_ChallengeStatus expected_cs; + + /** + * Index of the challenge we are solving + */ + unsigned int challenge_index; + + /** + * 0 for no plugin needed 1 for plugin needed to authenticate + */ + unsigned int mode; + + /** + * code we read in the file generated by the plugin + */ + char code[22]; + +}; + + +static void +challenge_answer_cb (void *af_cls, + const struct ANASTASIS_ChallengeStartResponse *csr) +{ + struct ChallengeState *cs = af_cls; + + cs->c = NULL; + if (csr->cs != cs->expected_cs) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Expected status %u, got %u\n", + cs->expected_cs, + csr->cs); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + switch (csr->cs) + { + case ANASTASIS_CHALLENGE_STATUS_SOLVED: + break; + case ANASTASIS_CHALLENGE_STATUS_INSTRUCTIONS: + { + FILE *file; + char *fn; + + if (0 == strcasecmp (csr->details.open_challenge.content_type, + "application/json")) + { + const char *filename; + json_t *in; + + in = json_loadb (csr->details.open_challenge.body, + csr->details.open_challenge.body_size, + JSON_REJECT_DUPLICATES, + NULL); + if (NULL == in) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + filename = json_string_value (json_object_get (in, + "filename")); + if (NULL == filename) + { + GNUNET_break (0); + json_decref (in); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + fn = GNUNET_strdup (filename); + json_decref (in); + } + else + { + fn = GNUNET_strndup (csr->details.open_challenge.body, + csr->details.open_challenge.body_size); + } + file = fopen (fn, + "r"); + if (NULL == file) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + fn); + GNUNET_free (fn); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + if (0 == fscanf (file, + "%21s", + cs->code)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "fscanf", + fn); + TALER_TESTING_interpreter_fail (cs->is); + fclose (file); + GNUNET_free (fn); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Read challenge answer `%s' from file `%s'\n", + cs->code, + fn); + TALER_TESTING_interpreter_next (cs->is); + GNUNET_break (0 == fclose (file)); + GNUNET_free (fn); + return; + } + case ANASTASIS_CHALLENGE_STATUS_PAYMENT_REQUIRED: + if (0 != strncmp (csr->details.payment_required.taler_pay_uri, + "taler+http://pay/", + strlen ("taler+http://pay/"))) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid payment URI `%s'\n", + csr->details.payment_required.taler_pay_uri); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + cs->payment_uri = GNUNET_strdup ( + csr->details.payment_required.taler_pay_uri); + { + struct TALER_MERCHANT_PayUriData pud; + + if (GNUNET_OK != + TALER_MERCHANT_parse_pay_uri (cs->payment_uri, + &pud)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + cs->order_id = GNUNET_strdup (pud.order_id); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (cs->order_id, + strlen (cs->order_id), + &cs->payment_order_req, + sizeof (cs->payment_order_req))) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + TALER_MERCHANT_parse_pay_uri_free (&pud); + } + TALER_TESTING_interpreter_next (cs->is); + return; + case ANASTASIS_CHALLENGE_STATUS_TRUTH_UNKNOWN: + break; + case ANASTASIS_CHALLENGE_STATUS_REDIRECT_FOR_AUTHENTICATION: + break; + case ANASTASIS_CHALLENGE_STATUS_SERVER_FAILURE: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + case ANASTASIS_CHALLENGE_STATUS_RATE_LIMIT_EXCEEDED: + break; + } + TALER_TESTING_interpreter_next (cs->is); +} + + +/** + * Run a "recover secret" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +challenge_answer_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct ChallengeState *cs = cls; + const struct ANASTASIS_Challenge *c; + const struct ANASTASIS_PaymentSecretP *ps; + + cs->is = is; + if (NULL != cs->challenge_ref) + { + const struct TALER_TESTING_Command *ref; + + ref = TALER_TESTING_interpreter_lookup_command ( + is, + cs->challenge_ref); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_challenge (ref, + cs->challenge_index, + &c)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + } + + if (NULL != cs->payment_ref) + { + const struct TALER_TESTING_Command *ref; + + ref = TALER_TESTING_interpreter_lookup_command (is, + cs->payment_ref); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_payment_secret (ref, + 0, + &ps)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + } + else + { + ps = NULL; + } + + cs->c = (struct ANASTASIS_Challenge *) c; + + if (1 == cs->mode) + { + const struct TALER_TESTING_Command *ref; + const char *answer; + unsigned long long code; + char dummy; + + ref = TALER_TESTING_interpreter_lookup_command (is, + cs->answer); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_code (ref, + 0, + &answer)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + if (1 != + sscanf (answer, + "%llu%c", + &code, + &dummy)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + if (GNUNET_OK != + ANASTASIS_challenge_answer2 (cs->c, + ps, + GNUNET_TIME_UNIT_ZERO, + code, + &challenge_answer_cb, + cs)) + { + GNUNET_break (0); + cs->c = NULL; + TALER_TESTING_interpreter_fail (cs->is); + return; + } + + } + else + { + if (GNUNET_OK != + ANASTASIS_challenge_answer (cs->c, + ps, + GNUNET_TIME_UNIT_ZERO, + cs->answer, + &challenge_answer_cb, + cs)) + { + GNUNET_break (0); + cs->c = NULL; + TALER_TESTING_interpreter_fail (cs->is); + return; + } + } +} + + +/** + * Run a "recover secret" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +challenge_start_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct ChallengeState *cs = cls; + const struct ANASTASIS_Challenge *c; + const struct TALER_TESTING_Command *ref; + const struct ANASTASIS_PaymentSecretP *ps; + + cs->is = is; + ref = TALER_TESTING_interpreter_lookup_command ( + is, + cs->challenge_ref); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_challenge (ref, + cs->challenge_index, + &c)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + if (NULL != cs->payment_ref) + { + const struct TALER_TESTING_Command *ref; + + ref = TALER_TESTING_interpreter_lookup_command (is, + cs->payment_ref); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_payment_secret (ref, + 0, + &ps)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } + } + else + { + ps = NULL; + } + if (GNUNET_OK != + ANASTASIS_challenge_start ((struct ANASTASIS_Challenge *) c, + ps, + GNUNET_TIME_UNIT_ZERO, + NULL, + &challenge_answer_cb, + cs)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (cs->is); + return; + } +} + + +/** + * Free the state of a "recover secret" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +challenge_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct ChallengeState *cs = cls; + + if (NULL != cs->c) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (challenge answer)\n", + cmd->label); + ANASTASIS_challenge_abort (cs->c); + cs->c = NULL; + } + GNUNET_free (cs->payment_uri); + GNUNET_free (cs->order_id); + GNUNET_free (cs); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param ret[out] result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static int +challenge_create_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct ChallengeState *cs = cls; + struct TALER_TESTING_Trait traits[] = { + ANASTASIS_TESTING_make_trait_code (0, + cs->code), + ANASTASIS_TESTING_make_trait_payment_secret (0, + &cs->payment_order_req), + TALER_TESTING_make_trait_url (TALER_TESTING_UT_TALER_URL, + cs->payment_uri), + TALER_TESTING_make_trait_order_id (0, + cs->order_id), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +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) +{ + struct ChallengeState *cs; + + cs = GNUNET_new (struct ChallengeState); + cs->expected_cs = expected_cs; + cs->challenge_ref = challenge_ref; + cs->payment_ref = payment_ref; + cs->challenge_index = challenge_index; + { + struct TALER_TESTING_Command cmd = { + .cls = cs, + .label = label, + .run = &challenge_start_run, + .cleanup = &challenge_cleanup, + .traits = &challenge_create_traits + }; + + return cmd; + } +} + + +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) +{ + struct ChallengeState *cs; + + cs = GNUNET_new (struct ChallengeState); + cs->expected_cs = expected_cs; + cs->challenge_ref = challenge_ref; + cs->payment_ref = payment_ref; + cs->answer = answer; + cs->challenge_index = challenge_index; + cs->mode = mode; + { + struct TALER_TESTING_Command cmd = { + .cls = cs, + .label = label, + .run = &challenge_answer_run, + .cleanup = &challenge_cleanup, + .traits = &challenge_create_traits + }; + + return cmd; + } +} + + +/* end of testing_cmd_challenge_answer.c */ diff --git a/src/testing/testing_cmd_policy_create.c b/src/testing/testing_cmd_policy_create.c new file mode 100644 index 0000000..fc9ed44 --- /dev/null +++ b/src/testing/testing_cmd_policy_create.c @@ -0,0 +1,208 @@ +/* + 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 +*/ +/** + * @file lib/testing_cmd_policy_create.c + * @brief command to execute the anastasis secret share service + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ + +#include "platform.h" +#include "anastasis_testing_lib.h" +#include +#include + + +/** + * State for a "policy create" CMD. + */ +struct PolicyCreateState +{ + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Label of this command. + */ + const char *label; + + /** + * References to upload commands of previous truth uploads. + */ + const char **cmd_label_array; + + /** + * Length of array of command labels (cmd_label_array). + */ + unsigned int cmd_label_array_length; + + /** + * Policy object + */ + struct ANASTASIS_Policy *policy; +}; + + +/** + * Run a "policy create" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +policy_create_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct PolicyCreateState *pcs = cls; + const struct ANASTASIS_Truth *truths[pcs->cmd_label_array_length]; + + GNUNET_assert (pcs->cmd_label_array_length > 0); + GNUNET_assert (NULL != pcs->cmd_label_array); + pcs->is = is; + if (NULL != pcs->cmd_label_array) + { + for (unsigned int i = 0; i < pcs->cmd_label_array_length; i++) + { + const struct TALER_TESTING_Command *ref; + const struct ANASTASIS_Truth *truth; + + ref = TALER_TESTING_interpreter_lookup_command (is, + pcs->cmd_label_array[i]); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pcs->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_truth (ref, + 0, + &truth)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pcs->is); + return; + } + GNUNET_assert (NULL != truth); + truths[i] = truth; + } + } + + pcs->policy = ANASTASIS_policy_create (truths, + pcs->cmd_label_array_length); + + if (NULL == pcs->policy) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (pcs->is); + return; + } + TALER_TESTING_interpreter_next (pcs->is); +} + + +/** + * Free the state of a "policy create" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +policy_create_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct PolicyCreateState *pcs = cls; + + GNUNET_free (pcs->cmd_label_array); + if (NULL != pcs->policy) + { + ANASTASIS_policy_destroy (pcs->policy); + pcs->policy = NULL; + } + GNUNET_free (pcs); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param ret[out] result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static int +policy_create_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct PolicyCreateState *pcs = cls; + struct TALER_TESTING_Trait traits[] = { + ANASTASIS_TESTING_make_trait_policy (0, + pcs->policy), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_policy_create (const char *label, + ...) +{ + struct PolicyCreateState *pcs; + va_list ap; + const char *truth_upload_cmd; + + pcs = GNUNET_new (struct PolicyCreateState); + pcs->label = label; + + va_start (ap, + label); + while (NULL != (truth_upload_cmd = va_arg (ap, const char *))) + { + GNUNET_array_append (pcs->cmd_label_array, + pcs->cmd_label_array_length, + truth_upload_cmd); + } + va_end (ap); + { + struct TALER_TESTING_Command cmd = { + .cls = pcs, + .label = label, + .run = &policy_create_run, + .cleanup = &policy_create_cleanup, + .traits = &policy_create_traits + }; + + return cmd; + } +} + + +/* end of testing_cmd_policy_create.c */ diff --git a/src/testing/testing_cmd_recover_secret.c b/src/testing/testing_cmd_recover_secret.c new file mode 100644 index 0000000..a95bdd2 --- /dev/null +++ b/src/testing/testing_cmd_recover_secret.c @@ -0,0 +1,518 @@ +/* + 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 +*/ +/** + * @file lib/testing_cmd_recover_secret.c + * @brief command to execute the anastasis recovery service + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis_testing_lib.h" +#include +#include + + +/** + * State for a "recover secret" CMD. + */ +struct RecoverSecretState +{ + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * URL of the anastasis backend. + */ + const char *anastasis_url; + + /** + * The /policy GET operation handle. + */ + struct ANASTASIS_Recovery *recovery; + + /** + * Reference to download command we expect to lookup. + */ + const char *download_reference; + + /** + * Reference to download command we expect to lookup. + */ + const char *core_secret_reference; + + /** + * Options for how we are supposed to do the download. + */ + enum ANASTASIS_TESTING_RecoverSecretOption rsopt; + + /** + * Identification data from the user + */ + json_t *id_data; + + /** + * Salt to be used to derive the id + */ + struct ANASTASIS_CRYPTO_ProviderSaltP *salt; + + /** + * Recovery information from the lookup + */ + struct ANASTASIS_RecoveryInformation *ri; + + /** + * Coresecret to check if decryption worked + */ + const void *core_secret; + + /** + * Task scheduled to wait for recovery to complete. + */ + struct GNUNET_SCHEDULER_Task *recovery_task; + + /** + * version of the recovery document + */ + unsigned int version; + + /** + * #GNUNET_OK if the secret was recovered, #GNUNET_SYSERR if + * recovery failed (yielded wrong secret). + */ + int recovered; +}; + + +/** + * 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 + */ +static void +policy_lookup_cb (void *cls, + const struct ANASTASIS_RecoveryInformation *ri) +{ + struct RecoverSecretState *rss = cls; + + rss->ri = (struct ANASTASIS_RecoveryInformation *) ri; + if (NULL == ri) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (rss->is); + return; + } + TALER_TESTING_interpreter_next (rss->is); +} + + +/** + * 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 + */ +static void +core_secret_cb (void *cls, + enum ANASTASIS_RecoveryStatus rc, + const void *secret, + size_t secret_size) +{ + struct RecoverSecretState *rss = cls; + + rss->recovery = NULL; + if (ANASTASIS_RS_SUCCESS != rc) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Recovery failed with status %d\n", + rc); + TALER_TESTING_interpreter_fail (rss->is); + return; + } + if (0 != memcmp (secret, + rss->core_secret, + secret_size)) + { + GNUNET_break (0); + rss->recovered = GNUNET_SYSERR; + if (NULL != rss->recovery_task) + { + GNUNET_SCHEDULER_cancel (rss->recovery_task); + rss->recovery_task = NULL; + TALER_TESTING_interpreter_fail (rss->is); + } + return; + } + rss->recovered = GNUNET_OK; + if (NULL != rss->recovery_task) + { + GNUNET_SCHEDULER_cancel (rss->recovery_task); + rss->recovery_task = NULL; + TALER_TESTING_interpreter_next (rss->is); + } +} + + +/** + * Run a "recover secret" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +recover_secret_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct RecoverSecretState *rss = cls; + const struct TALER_TESTING_Command *ref; + const struct ANASTASIS_CRYPTO_ProviderSaltP *salt; + rss->is = is; + + if (NULL != rss->download_reference) + { + ref = TALER_TESTING_interpreter_lookup_command + (is, + rss->download_reference); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (rss->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_salt (ref, + 0, + &salt)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (rss->is); + return; + } + } + if (NULL != rss->core_secret_reference) + { + ref = TALER_TESTING_interpreter_lookup_command + (is, + rss->core_secret_reference); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (rss->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_core_secret (ref, + 0, + &rss->core_secret)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (rss->is); + return; + } + } + rss->recovery = ANASTASIS_recovery_begin (is->ctx, + rss->id_data, + rss->version, + rss->anastasis_url, + salt, + &policy_lookup_cb, + rss, + &core_secret_cb, + rss); + if (NULL == rss->recovery) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (rss->is); + return; + } +} + + +/** + * Task to run the abort routine on the given @a cls object + * after the stack has fully unwound. + * + * @param cls a `struct ANASTASIS_Recovery *` + */ +static void +delayed_abort (void *cls) +{ + struct ANASTASIS_Recovery *recovery = cls; + + ANASTASIS_recovery_abort (recovery); +} + + +/** + * Free the state of a "recover secret" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure + * @param cmd command being freed. + */ +static void +recover_secret_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct RecoverSecretState *rss = cls; + + if (NULL != rss->recovery) + { + /* must run first, or at least before #core_secret_cb */ + (void) GNUNET_SCHEDULER_add_with_priority ( + GNUNET_SCHEDULER_PRIORITY_SHUTDOWN, + &delayed_abort, + rss->recovery); + rss->recovery = NULL; + } + if (NULL != rss->recovery_task) + { + GNUNET_SCHEDULER_cancel (rss->recovery_task); + rss->recovery_task = NULL; + } + json_decref (rss->id_data); + GNUNET_free (rss); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param ret[out] result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static int +recover_secret_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct RecoverSecretState *rss = cls; + + if (NULL == rss->ri) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (index >= rss->ri->cs_len) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + { + struct TALER_TESTING_Trait traits[] = { + ANASTASIS_TESTING_make_trait_challenge (index, + rss->ri->cs[index]), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); + } +} + + +/** + * Function called on timeout of the secret finishing operation. + * + * @param cls a `struct RecoverSecretState *` + */ +static void +recovery_fail (void *cls) +{ + struct RecoverSecretState *rss = cls; + + rss->recovery_task = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Timeout during secret recovery\n"); + TALER_TESTING_interpreter_fail (rss->is); +} + + +/** + * Wait @a delay for @a cmd to finish secret recovery. + * + * @param cmd command to wait on + * @param delay how long to wait at most + */ +static void +recover_secret_finish (struct TALER_TESTING_Command *cmd, + struct GNUNET_TIME_Relative delay) +{ + struct RecoverSecretState *rss = cmd->cls; + + GNUNET_assert (&recover_secret_run == cmd->run); + GNUNET_assert (NULL == rss->recovery_task); + switch (rss->recovered) + { + case GNUNET_OK: + TALER_TESTING_interpreter_next (rss->is); + break; + case GNUNET_NO: + rss->recovery_task = GNUNET_SCHEDULER_add_delayed (delay, + &recovery_fail, + rss); + break; + case GNUNET_SYSERR: + TALER_TESTING_interpreter_fail (rss->is); + break; + } +} + + +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) +{ + struct RecoverSecretState *rss; + + rss = GNUNET_new (struct RecoverSecretState); + rss->version = version; + rss->id_data = json_incref ((json_t *) id_data); + rss->rsopt = rso; + rss->anastasis_url = anastasis_url; + rss->download_reference = download_ref; + rss->core_secret_reference = core_secret_ref; + { + struct TALER_TESTING_Command cmd = { + .cls = rss, + .label = label, + .run = &recover_secret_run, + .cleanup = &recover_secret_cleanup, + .traits = &recover_secret_traits + }; + + return cmd; + } +} + + +/** + * State for a "recover secret finish" CMD. + */ +struct RecoverSecretFinishState +{ + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * URL of the anastasis backend. + */ + const char *recover_label; + + /** + * Timeout. + */ + struct GNUNET_TIME_Relative timeout; + +}; + + +/** + * Run a "recover secret finish" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +recover_secret_finish_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct RecoverSecretFinishState *rsfs = cls; + struct TALER_TESTING_Command *ref; + + rsfs->is = is; + ref = (struct TALER_TESTING_Command *) + TALER_TESTING_interpreter_lookup_command (is, + rsfs->recover_label); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (rsfs->is); + return; + } + recover_secret_finish (ref, + rsfs->timeout); +} + + +/** + * Free the state of a "recover secret finish" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure + * @param cmd command being freed. + */ +static void +recover_secret_finish_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct RecoverSecretFinishState *rsfs = cls; + + GNUNET_free (rsfs); +} + + +struct TALER_TESTING_Command +ANASTASIS_TESTING_cmd_recover_secret_finish ( + const char *label, + const char *recover_label, + struct GNUNET_TIME_Relative timeout) +{ + struct RecoverSecretFinishState *rsfs; + + rsfs = GNUNET_new (struct RecoverSecretFinishState); + rsfs->recover_label = recover_label; + rsfs->timeout = timeout; + { + struct TALER_TESTING_Command cmd = { + .cls = rsfs, + .label = label, + .run = &recover_secret_finish_run, + .cleanup = &recover_secret_finish_cleanup + }; + + return cmd; + } +} + + +/* end of testing_cmd_recover_secret.c */ diff --git a/src/testing/testing_cmd_secret_share.c b/src/testing/testing_cmd_secret_share.c new file mode 100644 index 0000000..b80006e --- /dev/null +++ b/src/testing/testing_cmd_secret_share.c @@ -0,0 +1,441 @@ +/* + 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 +*/ +/** + * @file lib/testing_cmd_secret_share.c + * @brief command to execute the anastasis secret share service + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ + +#include "platform.h" +#include "anastasis_testing_lib.h" +#include +#include +#include + + +/** + * State for a "secret share" CMD. + */ +struct SecretShareState +{ + /** + * Claim token we got back, if any. Otherwise all zeros. + */ + struct TALER_ClaimTokenP token; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Label of this command. + */ + const char *label; + + /** + * References to commands of previous policy creations. + */ + const char **cmd_label_array; + + /** + * Data to derive user identifier from. + */ + json_t *id_data; + + /** + * The core secret to backup/recover. + */ + const void *core_secret; + + /** + * URL of the anastasis backend. + */ + const char *anastasis_url; + + /** + * URL of a /config command for the @e anastasis_url. + */ + const char *config_ref; + + /** + * The /truth GET operation handle. + */ + struct ANASTASIS_SecretShare *sso; + + /** + * Reference to previous secret share command we expect to lookup. + */ + const char *prev_secret_share; + + /** + * closure for the payment callback + */ + void *spc_cls; + + /** + * closure for the result callback + */ + void *src_cls; + + /** + * Payment order ID we got back, if any. Otherwise NULL. + */ + char *payment_order_id; + + /** + * Size of core_secret. + */ + size_t core_secret_size; + + /** + * Length of array of command labels (cmd_label_array). + */ + unsigned int cmd_label_array_length; + + /** + * Expected status code. + */ + enum ANASTASIS_ShareStatus want_status; + + /** + * Options for how we are supposed to do the upload. + */ + enum ANASTASIS_TESTING_SecretShareOption ssopt; +}; + + +/** + * Function called with the results of a #ANASTASIS_secret_share(). + * + * @param cls closure + * @param sr result from the operation + */ +static void +secret_share_result_cb (void *cls, + const struct ANASTASIS_ShareResult *sr) +{ + struct SecretShareState *sss = cls; + + sss->sso = NULL; + if (sr->ss != sss->want_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to command %s in %s:%u\n", + sr->ss, + sss->is->commands[sss->is->ip].label, + __FILE__, + __LINE__); + TALER_TESTING_interpreter_fail (sss->is); + return; + } + switch (sr->ss) + { + case ANASTASIS_SHARE_STATUS_SUCCESS: + break; + case ANASTASIS_SHARE_STATUS_PAYMENT_REQUIRED: + { + struct TALER_MERCHANT_PayUriData pd; + + GNUNET_assert (0 < sr->details.payment_required.payment_requests_length); + if (GNUNET_OK != + TALER_MERCHANT_parse_pay_uri ( + sr->details.payment_required.payment_requests[0].payment_request_url, + &pd)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (sss->is); + return; + } + sss->payment_order_id = GNUNET_strdup (pd.order_id); + TALER_MERCHANT_parse_pay_uri_free (&pd); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order ID from Anastasis service is `%s'\n", + sss->payment_order_id); + } + case ANASTASIS_SHARE_STATUS_PROVIDER_FAILED: + break; + } + TALER_TESTING_interpreter_next (sss->is); +} + + +/** + * Run a "secret share" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +secret_share_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct SecretShareState *sss = cls; + const struct ANASTASIS_Policy *policies[sss->cmd_label_array_length]; + struct ANASTASIS_ProviderDetails pds; + + GNUNET_assert (sss->cmd_label_array_length > 0); + GNUNET_assert (NULL != sss->cmd_label_array); + sss->is = is; + if (NULL != sss->cmd_label_array) + { + for (unsigned int i = 0; i < sss->cmd_label_array_length; i++) + { + const struct TALER_TESTING_Command *ref; + const struct ANASTASIS_Policy *policy; + + ref = TALER_TESTING_interpreter_lookup_command (is, + sss->cmd_label_array[i]); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (sss->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_policy (ref, + 0, + &policy)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (sss->is); + return; + } + GNUNET_assert (NULL != policy); + policies[i] = policy; + } + } + + if (NULL != sss->prev_secret_share) + { + const struct TALER_TESTING_Command *ref; + const char *order_id; + + ref = TALER_TESTING_interpreter_lookup_command (is, + sss->prev_secret_share); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (sss->is); + return; + } + if (GNUNET_OK != + TALER_TESTING_get_trait_order_id (ref, + 0, + &order_id)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (sss->is); + return; + } + sss->payment_order_id = (char *) order_id; + + if (NULL == sss->payment_order_id) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (sss->is); + return; + } + } + + memset (&pds, + 0, + sizeof (pds)); + if (NULL != sss->payment_order_id) + { + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + sss->payment_order_id, + strlen (sss->payment_order_id), + &pds.payment_secret, + sizeof (struct ANASTASIS_PaymentSecretP))) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (sss->is); + GNUNET_free (sss->payment_order_id); + return; + } + GNUNET_free (sss->payment_order_id); + } + pds.provider_url = sss->anastasis_url; + { + const struct TALER_TESTING_Command *ref; + const struct ANASTASIS_CRYPTO_ProviderSaltP *salt; + + ref = TALER_TESTING_interpreter_lookup_command (is, + sss->config_ref); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (sss->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_salt (ref, + 0, + &salt)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (sss->is); + return; + } + pds.provider_salt = *salt; + } + + sss->sso = ANASTASIS_secret_share (is->ctx, + sss->id_data, + &pds, + 1, + policies, + sss->cmd_label_array_length, + false, + GNUNET_TIME_UNIT_ZERO, + &secret_share_result_cb, + sss, + "test-case", + sss->core_secret, + sss->core_secret_size); + if (NULL == sss->sso) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (sss->is); + return; + } +} + + +/** + * Free the state of a "secret share" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +secret_share_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct SecretShareState *sss = cls; + + if (NULL != sss->cmd_label_array) + { + GNUNET_free (sss->cmd_label_array); + } + if (NULL != sss->sso) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete\n", + cmd->label); + ANASTASIS_secret_share_cancel (sss->sso); + sss->sso = NULL; + } + json_decref (sss->id_data); + GNUNET_free (sss); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param ret[out] result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static int +secret_share_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct SecretShareState *sss = cls; + struct TALER_TESTING_Trait traits[] = { + TALER_TESTING_make_trait_claim_token (0, + &sss->token), + ANASTASIS_TESTING_make_trait_core_secret (0, + sss->core_secret), + TALER_TESTING_make_trait_order_id (0, + sss->payment_order_id), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +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, + enum ANASTASIS_ShareStatus want_status, + enum ANASTASIS_TESTING_SecretShareOption sso, + ...) +{ + struct SecretShareState *sss; + + sss = GNUNET_new (struct SecretShareState); + sss->want_status = want_status; + sss->ssopt = sso; + sss->anastasis_url = anastasis_url; + sss->config_ref = config_ref; + sss->label = label; + sss->id_data = json_incref ((json_t *) id_data); + sss->core_secret = core_secret; + sss->core_secret_size = core_secret_size; + sss->prev_secret_share = prev_secret_share; + + { + const char *policy_create_cmd; + va_list ap; + + va_start (ap, + sso); + while (NULL != (policy_create_cmd = va_arg (ap, const char *))) + { + GNUNET_array_append (sss->cmd_label_array, + sss->cmd_label_array_length, + policy_create_cmd); + } + va_end (ap); + } + { + struct TALER_TESTING_Command cmd = { + .cls = sss, + .label = label, + .run = &secret_share_run, + .cleanup = &secret_share_cleanup, + .traits = &secret_share_traits + }; + + return cmd; + } +} + + +/* end of testing_cmd_secret_share.c */ diff --git a/src/testing/testing_cmd_truth_upload.c b/src/testing/testing_cmd_truth_upload.c new file mode 100644 index 0000000..f9149d5 --- /dev/null +++ b/src/testing/testing_cmd_truth_upload.c @@ -0,0 +1,383 @@ +/* + 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 +*/ +/** + * @file lib/testing_cmd_truth_upload.c + * @brief command to execute the anastasis secret share service + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ + +#include "platform.h" +#include "anastasis_testing_lib.h" +#include +#include + + +/** + * State for a "truth upload" CMD. + */ +struct TruthUploadState +{ + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * URL of the anastasis backend. + */ + const char *anastasis_url; + + /** + * Label of this command. + */ + const char *label; + + /** + * The ID data to generate user identifier + */ + json_t *id_data; + + /** + * The escrow method + */ + const char *method; + + /** + * Instructions to be returned to client/user + * (e.g. "Look at your smartphone. SMS was sent to you") + */ + const char *instructions; + + /** + * Mime type of truth_data (eg. jpeg, string etc.) + */ + const char *mime_type; + + /** + * The truth_data (e.g. hash of answer to a secure question) + */ + void *truth_data; + + /** + * Requested order ID for this upload (if unpaid). + */ + struct ANASTASIS_PaymentSecretP payment_secret_response; + + /** + * Size of truth_data + */ + size_t truth_data_size; + + /** + * Expected status code. + */ + unsigned int http_status; + + /** + * The /truth POST operation handle. + */ + struct ANASTASIS_TruthUpload *tuo; + + /** + * closure for the payment callback + */ + void *tpc_cls; + + /** + * Reference to salt download. + */ + const char *salt_reference; + + /** + * Options for how we are supposed to do the upload. + */ + enum ANASTASIS_TESTING_TruthStoreOption tsopt; + + /** + * Truth object + */ + struct ANASTASIS_Truth *truth; +}; + + +/** + * Upload information + * caller MUST free 't' using ANASTASIS_truth_free() + * + * @param cls closure for callback + * @param t Truth object (contains provider url and truth public key) + * @param ud upload details, useful to continue in case of errors, NULL on success + */ +static void +truth_upload_cb (void *cls, + struct ANASTASIS_Truth *t, + const struct ANASTASIS_UploadDetails *ud) +{ + struct TruthUploadState *tus = cls; + + tus->tuo = NULL; + if (NULL == ud) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tus->is); + return; + } + if (ud->http_status != tus->http_status) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tus->is); + return; + } + if (MHD_HTTP_PAYMENT_REQUIRED == ud->http_status) + { + if (ANASTASIS_US_PAYMENT_REQUIRED != ud->us) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tus->is); + return; + } + tus->payment_secret_response = ud->details.payment.ps; + TALER_TESTING_interpreter_next (tus->is); + return; + } + if ( (ANASTASIS_US_SUCCESS == ud->us) && + (NULL == t) ) + { + GNUNET_break (0); + TALER_TESTING_interpreter_next (tus->is); + return; + } + tus->truth = t; + TALER_TESTING_interpreter_next (tus->is); +} + + +/** + * Run a "truth upload" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +truth_upload_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct TruthUploadState *tus = cls; + const struct TALER_TESTING_Command *ref; + const struct ANASTASIS_CRYPTO_ProviderSaltP *salt; + struct ANASTASIS_CRYPTO_UserIdentifierP user_id; + + tus->is = is; + if (NULL != tus->salt_reference) + { + ref = TALER_TESTING_interpreter_lookup_command + (is, + tus->salt_reference); + if (NULL == ref) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tus->is); + return; + } + if (GNUNET_OK != + ANASTASIS_TESTING_get_trait_salt (ref, + 0, + &salt)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tus->is); + return; + } + } + + ANASTASIS_CRYPTO_user_identifier_derive (tus->id_data, + salt, + &user_id); + + tus->tuo = ANASTASIS_truth_upload (is->ctx, + &user_id, + tus->anastasis_url, + tus->method, + tus->instructions, + tus->mime_type, + salt, + tus->truth_data, + tus->truth_data_size, + false, /* force payment */ + GNUNET_TIME_UNIT_ZERO, + &truth_upload_cb, + tus); + if (NULL == tus->tuo) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (tus->is); + } +} + + +/** + * Free the state of a "truth upload" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +truth_upload_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct TruthUploadState *tus = cls; + + if (NULL != tus->tuo) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete\n", + cmd->label); + ANASTASIS_truth_upload_cancel (tus->tuo); + tus->tuo = NULL; + } + if (NULL != tus->id_data) + { + json_decref (tus->id_data); + tus->id_data = NULL; + } + if (NULL != tus->truth) + { + ANASTASIS_truth_free (tus->truth); + tus->truth = NULL; + } + GNUNET_free (tus->truth_data); + GNUNET_free (tus); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param ret[out] result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static int +truth_upload_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct TruthUploadState *tus = cls; + struct TALER_TESTING_Trait traits[] = { + ANASTASIS_TESTING_make_trait_truth (0, + tus->truth), + ANASTASIS_TESTING_make_trait_payment_secret (0, + &tus->payment_secret_response), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +json_t * +ANASTASIS_TESTING_make_id_data_example (const char *id_data) +{ + json_t *id; + + id = json_pack ("{s:s}", + "id_data", id_data); + GNUNET_assert (NULL != id); + return id; +} + + +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 *salt_ref) +{ + struct TruthUploadState *tus; + + tus = GNUNET_new (struct TruthUploadState); + tus->label = label; + tus->http_status = http_status; + tus->tsopt = tso; + tus->anastasis_url = anastasis_url; + tus->salt_reference = salt_ref; + tus->id_data = json_incref ((json_t *) id_data); + tus->method = method; + tus->instructions = instructions; + tus->mime_type = mime_type; + tus->truth_data_size = truth_data_size; + tus->truth_data = GNUNET_memdup (truth_data, + truth_data_size); + { + struct TALER_TESTING_Command cmd = { + .cls = tus, + .label = label, + .run = &truth_upload_run, + .cleanup = &truth_upload_cleanup, + .traits = &truth_upload_traits + }; + + return cmd; + } +} + + +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) +{ + return ANASTASIS_TESTING_cmd_truth_upload (label, + anastasis_url, + id_data, + "question", + instructions, + mime_type, + answer, + strlen (answer), + http_status, + tso, + salt_ref); +} + + +/* end of testing_cmd_truth_upload.c */ diff --git a/src/testing/testing_trait_challenge.c b/src/testing/testing_trait_challenge.c new file mode 100644 index 0000000..5c40d8e --- /dev/null +++ b/src/testing/testing_trait_challenge.c @@ -0,0 +1,72 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 + +*/ +/** + * @file lib/testing_trait_challenge.c + * @brief traits to offer a challenge + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_CHALLENGE "anastasis-challenge" + +/** + * Obtain a challenge from @a cmd. + * + * @param cmd command to extract the challenge from. + * @param index the index of the challenge + * @param c[out] 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) +{ + return cmd->traits (cmd->cls, + (const void **) c, + ANASTASIS_TESTING_TRAIT_CHALLENGE, + index); +} + + +/** + * 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 *c) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_CHALLENGE, + .ptr = (const void *) c + }; + return ret; +} + + +/* end of testing_trait_challenge.c */ diff --git a/src/testing/testing_trait_core_secret.c b/src/testing/testing_trait_core_secret.c new file mode 100644 index 0000000..100a249 --- /dev/null +++ b/src/testing/testing_trait_core_secret.c @@ -0,0 +1,74 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 + +*/ +/** + * @file lib/testing_trait_core_secret.c + * @brief traits to offer the core secret + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_CORE_SECRET "anastasis-core-secret" + + +/** + * 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 s[out] 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) +{ + return cmd->traits (cmd->cls, + s, + ANASTASIS_TESTING_TRAIT_CORE_SECRET, + index); +} + + +/** + * 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) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_CORE_SECRET, + .ptr = s + }; + return ret; +} + + +/* end of testing_trait_core_secret.c */ diff --git a/src/testing/testing_trait_policy.c b/src/testing/testing_trait_policy.c new file mode 100644 index 0000000..45e773c --- /dev/null +++ b/src/testing/testing_trait_policy.c @@ -0,0 +1,73 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 + +*/ +/** + * @file lib/testing_trait_policy.c + * @brief traits to offer a policy + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_POLICY "anastasis-policy" + + +/** + * Obtain a policy from @a cmd. + * + * @param cmd command to extract the policy from. + * @param index the index of the policy + * @param t[out] 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) +{ + return cmd->traits (cmd->cls, + (const void **) p, + ANASTASIS_TESTING_TRAIT_POLICY, + index); +} + + +/** + * 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) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_POLICY, + .ptr = (const void *) p + }; + return ret; +} + + +/* end of testing_trait_policy.c */ diff --git a/src/testing/testing_trait_truth.c b/src/testing/testing_trait_truth.c new file mode 100644 index 0000000..51696e1 --- /dev/null +++ b/src/testing/testing_trait_truth.c @@ -0,0 +1,73 @@ +/* + This file is part of TALER + Copyright (C) 2020 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 + +*/ +/** + * @file lib/testing_trait_truth.c + * @brief traits to offer a truth + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include "platform.h" +#include "anastasis_testing_lib.h" + +#define ANASTASIS_TESTING_TRAIT_TRUTH "anastasis-truth" + + +/** + * Obtain a truth from @a cmd. + * + * @param cmd command to extract the truth from. + * @param index the index of the truth + * @param t[out] 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) +{ + return cmd->traits (cmd->cls, + (const void **) t, + ANASTASIS_TESTING_TRAIT_TRUTH, + index); +} + + +/** + * 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) +{ + struct TALER_TESTING_Trait ret = { + .index = index, + .trait_name = ANASTASIS_TESTING_TRAIT_TRUTH, + .ptr = (const void *) t + }; + return ret; +} + + +/* end of testing_trait_truth.c */ diff --git a/src/util/Makefile.am b/src/util/Makefile.am new file mode 100644 index 0000000..657ec0c --- /dev/null +++ b/src/util/Makefile.am @@ -0,0 +1,57 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include $(LIBGCRYPT_CFLAGS) + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +pkgcfgdir = $(prefix)/share/anastasis/config.d/ + +pkgcfg_DATA = \ + paths.conf + +EXTRA_DIST = \ + anastasis-config.in \ + $(pkgcfg_DATA) + +edit_script = $(SED) -e 's,%libdir%,$(libdir),'g $(NULL) + +anastasis-config: anastasis-config.in + rm -f $@ $@.tmp && \ + $(edit_script) $< >$@.tmp && \ + chmod a-w+x $@.tmp && \ + mv $@.tmp $@ + +bin_SCRIPTS = \ + anastasis-config + +lib_LTLIBRARIES = \ + libanastasisutil.la + +libanastasisutil_la_SOURCES = \ + anastasis_crypto.c \ + os_installation.c +libanastasisutil_la_LIBADD = \ + -lgnunetutil \ + $(LIBGCRYPT_LIBS) \ + -ljansson \ + -ltalerutil \ + $(XLIB) +libanastasisutil_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined + +check_PROGRAMS = \ + test_anastasis_crypto + +TESTS = \ + $(check_PROGRAMS) + +test_anastasis_crypto_SOURCES = \ + test_anastasis_crypto.c +test_anastasis_crypto_LDADD = \ + -lgnunetutil \ + -ltalerutil \ + libanastasisutil.la \ + $(XLIB) diff --git a/src/util/anastasis-config.in b/src/util/anastasis-config.in new file mode 100644 index 0000000..0e94921 --- /dev/null +++ b/src/util/anastasis-config.in @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -eu + +if ! type gnunet-config >/dev/null; then + echo "$0 needs gnunet-config to be installed" + exit 1 +fi + +GC=`which gnunet-config` +export LD_PRELOAD=${LD_PRELOAD:-}:%libdir%/libanastasisutil.so +exec gnunet-config "$@" diff --git a/src/util/anastasis_crypto.c b/src/util/anastasis_crypto.c new file mode 100644 index 0000000..ace0162 --- /dev/null +++ b/src/util/anastasis_crypto.c @@ -0,0 +1,637 @@ +/* + 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 +*/ +/** + * @file lib/anastasis_crypto.c + * @brief anastasis crypto api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ + +#include "platform.h" +#include "anastasis_crypto_lib.h" +#include +#include +#include +#include + +#if defined(DEBUG) || defined(DEBUG2) +#define SCRYPT_ITERATION 1 + +#else +#define SCRYPT_ITERATION 1000 +#endif + + +void +ANASTASIS_hash_answer (uint64_t code, + struct GNUNET_HashCode *hashed_code) +{ + char cbuf[40]; + + GNUNET_snprintf (cbuf, + sizeof (cbuf), + "%llu", + (unsigned long long) code); + GNUNET_CRYPTO_hash (cbuf, + strlen (cbuf), + hashed_code); +} + + +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) +{ + struct GNUNET_HashCode pow; + + GNUNET_CRYPTO_pow_hash (&salt->pow_salt, + answer, + strlen (answer), + &pow); + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_kdf ( + result, + sizeof (*result), + "Anastasis-secure-question-uuid-salting", + strlen ("Anastasis-secure-question-uuid-salting"), + &pow, + sizeof (pow), + uuid, + sizeof (*uuid), + NULL, + 0)); +} + + +/** + * Compute @a key and @a iv. + * + * @param key_material key for calculation + * @param key_m_len length of key + * @param nonce nonce for calculation + * @param salt salt value for calculation + * @param[out] key where to write the en-/description key + * @param[out] iv where to write the IV + */ +static void +get_iv_key (const void *key_material, + size_t key_m_len, + const struct ANASTASIS_CRYPTO_NonceP *nonce, + const char *salt, + const struct ANASTASIS_CRYPTO_SymKeyP *key, + struct ANASTASIS_CRYPTO_IvP *iv) +{ + char res[sizeof (struct ANASTASIS_CRYPTO_SymKeyP) + + sizeof (struct ANASTASIS_CRYPTO_IvP)]; + + if (GNUNET_YES != + GNUNET_CRYPTO_hkdf (res, + sizeof (res), + GCRY_MD_SHA512, + GCRY_MD_SHA256, + key_material, + key_m_len, + nonce, + sizeof (struct ANASTASIS_CRYPTO_NonceP), + salt, + strlen (salt), + NULL, + 0)) + { + GNUNET_break (0); + return; + } + memcpy ((void *) key, + res, + sizeof (*key)); + memcpy (iv, + &res[sizeof (*key)], + sizeof (*iv)); +} + + +/** + * Encryption of data like recovery document etc. + * + * @param nonce value to use for the nonce + * @param key key which is used to derive a key/iv pair from + * @param key_len length of key + * @param data data to encrypt + * @param data_size size of the data + * @param salt salt value which is used for key derivation + * @param res[out] ciphertext output + * @param res_size[out] size of the ciphertext + */ +static void +anastasis_encrypt (const struct ANASTASIS_CRYPTO_NonceP *nonce, + const void *key, + size_t key_len, + const void *data, + size_t data_size, + const char *salt, + void **res, + size_t *res_size) +{ + struct ANASTASIS_CRYPTO_NonceP *nonceptr; + gcry_cipher_hd_t cipher; + struct ANASTASIS_CRYPTO_SymKeyP sym_key; + struct ANASTASIS_CRYPTO_IvP iv; + int rc; + struct ANASTASIS_CRYPTO_AesTagP *tag; + char *ciphertext; + + *res_size = data_size + + sizeof (struct ANASTASIS_CRYPTO_NonceP) + + sizeof (struct ANASTASIS_CRYPTO_AesTagP); + if (*res_size <= data_size) + { + GNUNET_break (0); + return; + } + *res = GNUNET_malloc (*res_size); + if (*res_size != data_size + + sizeof (struct ANASTASIS_CRYPTO_NonceP) + + sizeof (struct ANASTASIS_CRYPTO_AesTagP)) + { + GNUNET_break (0); + return; + } + nonceptr = (struct ANASTASIS_CRYPTO_NonceP *) *res; + tag = (struct ANASTASIS_CRYPTO_AesTagP *) &nonceptr[1]; + ciphertext = (char *) &tag[1]; + memcpy (nonceptr, + nonce, + sizeof (*nonce)); + get_iv_key (key, + key_len, + nonce, + salt, + &sym_key, + &iv); + GNUNET_assert (0 == + gcry_cipher_open (&cipher, + GCRY_CIPHER_AES256, + GCRY_CIPHER_MODE_GCM, + 0)); + rc = gcry_cipher_setkey (cipher, + &sym_key, + sizeof (sym_key)); + GNUNET_assert ((0 == rc) || ((char) rc == GPG_ERR_WEAK_KEY)); + rc = gcry_cipher_setiv (cipher, + &iv, + sizeof (iv)); + GNUNET_assert ((0 == rc) || ((char) rc == GPG_ERR_WEAK_KEY)); + + GNUNET_assert (0 == + gcry_cipher_encrypt (cipher, + ciphertext, + data_size, + data, + data_size)); + GNUNET_assert (0 == + gcry_cipher_gettag (cipher, + tag, + sizeof (struct ANASTASIS_CRYPTO_AesTagP))); + gcry_cipher_close (cipher); +} + + +/** + * Decryption of data like encrypted recovery document etc. + * + * @param key key which is used to derive a key/iv pair from + * @param key_len length of key + * @param data data to decrypt + * @param data_size size of the data + * @param salt salt value which is used for key derivation + * @param res[out] plaintext output + * @param res_size[out] size of the plaintext + */ +static void +anastasis_decrypt (const void *key, + size_t key_len, + const void *data, + size_t data_size, + const char *salt, + void **res, + size_t *res_size) +{ + const struct ANASTASIS_CRYPTO_NonceP *nonce; + gcry_cipher_hd_t cipher; + const struct ANASTASIS_CRYPTO_SymKeyP sym_key; + struct ANASTASIS_CRYPTO_IvP iv; + int rc; + const struct ANASTASIS_CRYPTO_AesTagP *tag; + const char *ciphertext; + + *res_size = data_size + - sizeof (struct ANASTASIS_CRYPTO_NonceP) + - sizeof (struct ANASTASIS_CRYPTO_AesTagP); + if (*res_size >= data_size) + { + GNUNET_break (0); + return; + } + *res = GNUNET_malloc (*res_size); + if (*res_size != data_size + - sizeof (struct ANASTASIS_CRYPTO_NonceP) + - sizeof (struct ANASTASIS_CRYPTO_AesTagP)) + { + GNUNET_break (0); + GNUNET_free (*res); + return; + } + + nonce = (const struct ANASTASIS_CRYPTO_NonceP *) data; + tag = (struct ANASTASIS_CRYPTO_AesTagP *) &nonce[1]; + ciphertext = (const char *) &tag[1]; + get_iv_key (key, + key_len, + nonce, + salt, + &sym_key, + &iv); + GNUNET_assert (0 == + gcry_cipher_open (&cipher, + GCRY_CIPHER_AES256, + GCRY_CIPHER_MODE_GCM, + 0)); + rc = gcry_cipher_setkey (cipher, + &sym_key, + sizeof (sym_key)); + GNUNET_assert ((0 == rc) || ((char) rc == GPG_ERR_WEAK_KEY)); + + rc = gcry_cipher_setiv (cipher, + &iv, + sizeof (iv)); + GNUNET_assert ((0 == rc) || ((char) rc == GPG_ERR_WEAK_KEY)); + + GNUNET_assert (0 == gcry_cipher_decrypt (cipher, + *res, + *res_size, + ciphertext, + *res_size)); + if (0 != + gcry_cipher_checktag (cipher, + tag, + sizeof (struct ANASTASIS_CRYPTO_AesTagP))) + { + GNUNET_break (0); + GNUNET_free (*res); + return; + } + gcry_cipher_close (cipher); +} + + +void +ANASTASIS_CRYPTO_user_identifier_derive ( + const json_t *id_data, + const struct ANASTASIS_CRYPTO_ProviderSaltP *server_salt, + struct ANASTASIS_CRYPTO_UserIdentifierP *id) +{ + char *json_enc; + struct GNUNET_HashCode hash; + + json_enc = json_dumps (id_data, + JSON_COMPACT | JSON_SORT_KEYS); + GNUNET_assert (NULL != json_enc); + GNUNET_CRYPTO_pow_hash (&server_salt->salt, + json_enc, + strlen (json_enc), + &hash); + id->hash = hash; + free (json_enc); +} + + +void +ANASTASIS_CRYPTO_account_private_key_derive ( + const struct ANASTASIS_CRYPTO_UserIdentifierP *id, + struct ANASTASIS_CRYPTO_AccountPrivateKeyP *priv_key) +{ + /* priv_key = ver_secret */ + if (GNUNET_YES != + GNUNET_CRYPTO_hkdf (&priv_key->priv, + sizeof (priv_key->priv), + GCRY_MD_SHA512, + GCRY_MD_SHA256, + id, + sizeof (struct ANASTASIS_CRYPTO_UserIdentifierP), + "ver", + strlen ("ver"), + NULL, + 0)) + { + GNUNET_break (0); + return; + } + /* go from ver_secret to proper private key (eddsa_d_to_a() in spec) */ + priv_key->priv.d[0] = (priv_key->priv.d[0] & 0x7f) | 0x40; + priv_key->priv.d[31] &= 0xf8; +} + + +void +ANASTASIS_CRYPTO_account_public_key_derive ( + const struct ANASTASIS_CRYPTO_UserIdentifierP *id, + struct ANASTASIS_CRYPTO_AccountPublicKeyP *pub_key) +{ + struct ANASTASIS_CRYPTO_AccountPrivateKeyP priv; + + ANASTASIS_CRYPTO_account_private_key_derive (id, + &priv); + GNUNET_CRYPTO_eddsa_key_get_public (&priv.priv, + &pub_key->pub); +} + + +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) +{ + const char *salt = "erd"; + struct ANASTASIS_CRYPTO_NonceP nonce; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &nonce, + sizeof (nonce)); + anastasis_encrypt (&nonce, + id, + sizeof (struct ANASTASIS_CRYPTO_UserIdentifierP), + rec_doc, + rd_size, + salt, + enc_rec_doc, + erd_size); +} + + +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) +{ + const char *salt = "erd"; + + anastasis_decrypt (id, + sizeof (struct ANASTASIS_CRYPTO_UserIdentifierP), + enc_rec_doc, + erd_size, + salt, + rec_doc, + rd_size); +} + + +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) +{ + const char *salt = "eks"; + size_t eks_size = 0; + void *eks = NULL; + struct ANASTASIS_CRYPTO_NonceP nonce; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &nonce, + sizeof (nonce)); + anastasis_encrypt (&nonce, + id, + sizeof (struct ANASTASIS_CRYPTO_UserIdentifierP), + key_share, + sizeof (struct ANASTASIS_CRYPTO_KeyShareP), + (NULL == xsalt) ? salt : xsalt, + &eks, + &eks_size); + GNUNET_assert (eks_size == + sizeof (struct ANASTASIS_CRYPTO_EncryptedKeyShareP)); + memcpy (enc_key_share, + eks, + sizeof (struct ANASTASIS_CRYPTO_EncryptedKeyShareP)); + GNUNET_free (eks); +} + + +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) +{ + const char *salt = "eks"; + size_t ks_size = 0; + void *ks = NULL; + + anastasis_decrypt (id, + sizeof (struct ANASTASIS_CRYPTO_UserIdentifierP), + enc_key_share, + sizeof (struct ANASTASIS_CRYPTO_EncryptedKeyShareP), + (NULL == xsalt) ? salt : xsalt, + &ks, + &ks_size); + GNUNET_assert (ks_size == + sizeof (struct ANASTASIS_CRYPTO_KeyShareP)); + memcpy (key_share, + ks, + sizeof (struct ANASTASIS_CRYPTO_KeyShareP)); + GNUNET_free (ks); +} + + +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) +{ + const char *salt = "ect"; + + anastasis_encrypt (nonce, + truth_enc_key, + sizeof (struct ANASTASIS_CRYPTO_TruthKeyP), + truth, + truth_size, + salt, + enc_truth, + ect_size); +} + + +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) +{ + const char *salt = "ect"; + + anastasis_decrypt (truth_enc_key, + sizeof (struct ANASTASIS_CRYPTO_TruthKeyP), + enc_truth, + ect_size, + salt, + truth, + truth_size); +} + + +void +ANASTASIS_CRYPTO_keyshare_create ( + struct ANASTASIS_CRYPTO_KeyShareP *key_share) +{ + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + key_share, + sizeof (struct ANASTASIS_CRYPTO_KeyShareP)); +} + + +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) +{ + GNUNET_CRYPTO_hkdf (policy_key, + sizeof (*policy_key), + GCRY_MD_SHA512, + GCRY_MD_SHA256, + key_shares, + keyshare_length * sizeof (*key_shares), + salt, + sizeof (*salt), + NULL, 0); +} + + +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) +{ + struct GNUNET_CRYPTO_SymmetricSessionKey sk; + struct GNUNET_CRYPTO_SymmetricInitializationVector iv; + struct GNUNET_HashCode master_key; + + *enc_core_secret = GNUNET_malloc (core_secret_size); + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + &master_key, + sizeof (struct GNUNET_HashCode)); + GNUNET_CRYPTO_hash_to_aes_key (&master_key, + &sk, + &iv); + GNUNET_assert (GNUNET_SYSERR != + GNUNET_CRYPTO_symmetric_encrypt (core_secret, + core_secret_size, + &sk, + &iv, + *enc_core_secret)); + for (unsigned int i = 0; i < policy_keys_length; i++) + { + struct GNUNET_CRYPTO_SymmetricSessionKey i_sk; + struct GNUNET_CRYPTO_SymmetricInitializationVector i_iv; + struct GNUNET_HashCode key = policy_keys[i].key; + + GNUNET_CRYPTO_hash_to_aes_key (&key, + &i_sk, + &i_iv); + GNUNET_assert ( + GNUNET_SYSERR != + GNUNET_CRYPTO_symmetric_encrypt (&master_key, + sizeof (struct GNUNET_HashCode), + &i_sk, + &i_iv, + &encrypted_master_keys[i])); + } +} + + +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) +{ + struct GNUNET_CRYPTO_SymmetricSessionKey mk_sk; + struct GNUNET_CRYPTO_SymmetricInitializationVector mk_iv; + struct GNUNET_CRYPTO_SymmetricSessionKey core_sk; + struct GNUNET_CRYPTO_SymmetricInitializationVector core_iv; + struct GNUNET_HashCode master_key; + struct GNUNET_HashCode key = policy_key->key; + + *core_secret = GNUNET_malloc (encrypted_core_secret_size); + GNUNET_CRYPTO_hash_to_aes_key (&key, + &mk_sk, + &mk_iv); + GNUNET_assert ( + GNUNET_SYSERR != + GNUNET_CRYPTO_symmetric_decrypt ( + encrypted_master_key, + sizeof (struct ANASTASIS_CRYPTO_EncryptedMasterKeyP), + &mk_sk, + &mk_iv, + &master_key)); + GNUNET_CRYPTO_hash_to_aes_key (&master_key, + &core_sk, + &core_iv); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "At %s:%d encrypted core secret is %s-%llu b\n", __FILE__, + __LINE__, + TALER_b2s (encrypted_core_secret, encrypted_core_secret_size), + (unsigned long long) encrypted_core_secret_size); + *core_secret_size = GNUNET_CRYPTO_symmetric_decrypt (encrypted_core_secret, + encrypted_core_secret_size, + &core_sk, + &core_iv, + *core_secret); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "At %s:%d decrypted core secret is %s-%llu b\n", __FILE__, + __LINE__, + TALER_b2s (*core_secret, *core_secret_size), + (unsigned long long) *core_secret_size); + GNUNET_assert (GNUNET_SYSERR != *core_secret_size); +} + + +/* end of anastasis_crypto.c */ diff --git a/src/util/os_installation.c b/src/util/os_installation.c new file mode 100644 index 0000000..d9608d3 --- /dev/null +++ b/src/util/os_installation.c @@ -0,0 +1,71 @@ +/* + This file is part of GNU 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 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. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +/** + * @file os_installation.c + * @brief initialize libgnunet OS subsystem for Anastasis. + * @author Christian Grothoff + */ +#include "platform.h" +#include + + +/** + * Default project data used for installation path detection + * for GNU Anastasis. + */ +static const struct GNUNET_OS_ProjectData anastasis_pd = { + .libname = "libanastasisutil", + .project_dirname = "anastasis", + .binary_name = "anastasis-httpd", + .env_varname = "ANASTASIS_PREFIX", + .base_config_varname = "ANASTASIS_BASE_CONFIG", + .bug_email = "contact@anastasis.lu", + .homepage = "https://anastasis.lu/", + .config_file = "anastasis.conf", + .user_config_file = "~/.config/anastasis.conf", + .version = PACKAGE_VERSION, + .is_gnu = 0, + .gettext_domain = "anastasis", + .gettext_path = NULL, +}; + + +/** + * Return default project data used by Anastasis. + */ +const struct GNUNET_OS_ProjectData * +ANASTASIS_project_data_default (void) +{ + return &anastasis_pd; +} + + +/** + * Initialize libanastasisutil. + */ +void __attribute__ ((constructor)) +ANASTASIS_OS_init () +{ + GNUNET_OS_init (&anastasis_pd); +} + + +/* end of os_installation.c */ diff --git a/src/util/paths.conf b/src/util/paths.conf new file mode 100644 index 0000000..c62a24a --- /dev/null +++ b/src/util/paths.conf @@ -0,0 +1,29 @@ +# This file is in the public domain. +# +[PATHS] +# The PATHS section is special, as filenames including $-expression are +# expanded using the values from PATHS or the system environment (PATHS +# is checked first). Anastasis also supports expanding $-expressions using +# defaults with the syntax "${VAR:-default}". Here, "default" can again +# be a $-expression. +# +# We usually want $HOME for $ANASTASIS_HOME, but we allow testcases to +# easily override this by setting $ANASTASIS_TEST_HOME. +# +ANASTASIS_HOME = ${ANASTASIS_TEST_HOME:-${HOME:-${USERPROFILE}}} + +# see XDG Base Directory Specification at +# http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html +# for how these should be used. + +# Persistent data storage +ANASTASIS_DATA_HOME = ${XDG_DATA_HOME:-$ANASTASIS_HOME/.local/share}/anastasis/ + +# Configuration files +ANASTASIS_CONFIG_HOME = ${XDG_CONFIG_HOME:-$ANASTASIS_HOME/.config}/anastasis/ + +# Cached data, no big deal if lost +ANASTASIS_CACHE_HOME = ${XDG_CACHE_HOME:-$ANASTASIS_HOME/.cache}/anastasis/ + +# Runtime data (always lost on system boot) +ANASTASIS_RUNTIME_DIR = ${TMPDIR:-${TMP:-/tmp}}/anastasis-system-runtime/ diff --git a/src/util/test_anastasis_crypto.c b/src/util/test_anastasis_crypto.c new file mode 100644 index 0000000..9a6a98c --- /dev/null +++ b/src/util/test_anastasis_crypto.c @@ -0,0 +1,346 @@ +/* + This file is part of TALER + Copyright (C) 2014-2020 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 + +*/ + +/** + * @file lib/test_anastasis_api.c + * @brief testcase to test anastasis' HTTP API interface + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include +#include +#include "anastasis_crypto_lib.h" + +/** + * Testing derivation of the user identifier + */ +static int +test_user_identifier_derive (void) +{ + json_t *id_data_1; + json_t *id_data_2; + json_t *id_data_3; + struct ANASTASIS_CRYPTO_UserIdentifierP id_1; + struct ANASTASIS_CRYPTO_UserIdentifierP id_2; + struct ANASTASIS_CRYPTO_UserIdentifierP id_3; + struct ANASTASIS_CRYPTO_ProviderSaltP server_salt; + + char *salt_str = "Server-Salt-Test"; + + GNUNET_memcpy (&server_salt, + salt_str, + strlen (salt_str)); + // sample data 1 + id_data_1 = json_object (); + json_object_set_new (id_data_1, "arg1", json_string ("Hallo")); + // sample data 2, equal to sample data 1 + id_data_2 = json_object (); + json_object_set_new (id_data_2, "arg1", json_string ("Hallo")); + // sample data 3, differs + id_data_3 = json_object (); + json_object_set_new (id_data_3, "arg1", json_string ("Hallo2")); + + ANASTASIS_CRYPTO_user_identifier_derive (id_data_1, + &server_salt, + &id_1); + ANASTASIS_CRYPTO_user_identifier_derive (id_data_2, + &server_salt, + &id_2); + ANASTASIS_CRYPTO_user_identifier_derive (id_data_3, + &server_salt, + &id_3); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "UserIdentifier_1: %s\n", + TALER_B2S (&id_1)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "UserIdentifier_2: %s\n", + TALER_B2S (&id_2)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "UserIdentifier_3: %s\n", + TALER_B2S (&id_3)); + GNUNET_assert (0 == GNUNET_memcmp (&id_1, &id_2)); + GNUNET_assert (0 != GNUNET_memcmp (&id_1, &id_3)); + json_decref (id_data_1); + json_decref (id_data_2); + json_decref (id_data_3); + return 0; +} + + +/** + * Testing the encryption of an recovery document and the + * decryption of the encrypted recovery document + */ +static int +test_recovery_document (void) +{ + void *ciphertext; + size_t size_ciphertext; + void *plaintext; + size_t size_plaintext; + struct ANASTASIS_CRYPTO_UserIdentifierP id; + struct ANASTASIS_CRYPTO_ProviderSaltP server_salt; + int ret; + + json_t *id_data = json_object (); + const char *test = "TEST_ERD"; + char *salt_str = "Server-Salt-Test"; + + GNUNET_memcpy (&server_salt, + salt_str, + strlen (salt_str)); + json_object_set_new (id_data, "arg1", json_string ("ID_DATA")); + ANASTASIS_CRYPTO_user_identifier_derive (id_data, + &server_salt, + &id); + ANASTASIS_CRYPTO_recovery_document_encrypt (&id, + test, + strlen (test), + &ciphertext, + &size_ciphertext); + + ANASTASIS_CRYPTO_recovery_document_decrypt (&id, + ciphertext, + size_ciphertext, + &plaintext, + &size_plaintext); + GNUNET_assert (strlen (test) == size_plaintext); + ret = strncmp (plaintext, test, strlen (test)); + json_decref (id_data); + GNUNET_free (ciphertext); + GNUNET_free (plaintext); + return ret; +} + + +static int +test_key_share (void) +{ + struct ANASTASIS_CRYPTO_EncryptedKeyShareP ciphertext; + struct ANASTASIS_CRYPTO_KeyShareP plaintext; + struct ANASTASIS_CRYPTO_UserIdentifierP id; + struct ANASTASIS_CRYPTO_KeyShareP key_share; + struct ANASTASIS_CRYPTO_KeyShareP key_share_1; + struct ANASTASIS_CRYPTO_KeyShareP key_share_2; + + // testing creation of keyshares + ANASTASIS_CRYPTO_keyshare_create (&key_share_1); + ANASTASIS_CRYPTO_keyshare_create (&key_share_2); + GNUNET_assert (0 != + GNUNET_memcmp (&key_share_1, + &key_share_2)); + + // testing of enc-/decryption of a keyshare + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &id, + sizeof (struct ANASTASIS_CRYPTO_UserIdentifierP)); + ANASTASIS_CRYPTO_keyshare_create (&key_share); + ANASTASIS_CRYPTO_keyshare_encrypt (&key_share, + &id, + NULL, + &ciphertext); + ANASTASIS_CRYPTO_keyshare_decrypt (&ciphertext, + &id, + NULL, + &plaintext); + return GNUNET_memcmp (&key_share, + &plaintext); +} + + +static int +test_truth (void) +{ + const char *test = "TEST_TRUTH"; + void *ciphertext; + size_t size_ciphertext; + void *plaintext; + size_t size_plaintext; + struct ANASTASIS_CRYPTO_TruthKeyP truth_enc_key; + int ret; + struct ANASTASIS_CRYPTO_NonceP nonce; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "TRUTH_BEFORE: %s\n", + TALER_b2s (test, + strlen (test))); + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &truth_enc_key, + sizeof (struct ANASTASIS_CRYPTO_TruthKeyP)); + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &nonce, + sizeof (nonce)); + ANASTASIS_CRYPTO_truth_encrypt (&nonce, + &truth_enc_key, + test, + strlen (test), + &ciphertext, + &size_ciphertext); + + ANASTASIS_CRYPTO_truth_decrypt (&truth_enc_key, + ciphertext, + size_ciphertext, + &plaintext, + &size_plaintext); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "TRUTH_AFTER: %s\n", + TALER_b2s (plaintext, size_plaintext)); + GNUNET_assert (strlen (test) == size_plaintext); + ret = strncmp (plaintext, test, strlen (test)); + GNUNET_free (ciphertext); + GNUNET_free (plaintext); + return ret; +} + + +static int +test_core_secret (void) +{ + const char *test = "TEST_CORE_SECRET"; + const char *test_wrong = "TEST_CORE_WRONG"; + void *enc_core_secret; + unsigned int policy_keys_length = 5; + struct ANASTASIS_CRYPTO_MasterSaltP salt; + struct ANASTASIS_CRYPTO_EncryptedMasterKeyP + encrypted_master_keys[policy_keys_length]; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, + &salt, + sizeof (salt)); + + // construction of PolicyKey-array + struct ANASTASIS_CRYPTO_PolicyKeyP policy_keys[policy_keys_length]; + for (unsigned int i = 0; i < policy_keys_length; i++) + { + // construction of KeyShare-array + unsigned int keyshare_length = 5; + struct ANASTASIS_CRYPTO_KeyShareP keyshares[keyshare_length]; + for (unsigned int j = 0; j < keyshare_length; j++) + { + ANASTASIS_CRYPTO_keyshare_create (&keyshares[j]); + if (j > 0) + GNUNET_assert (0 != + GNUNET_memcmp (&keyshares[j - 1], &keyshares[j])); + } + + // derive policy-keys + ANASTASIS_CRYPTO_policy_key_derive ((struct + ANASTASIS_CRYPTO_KeyShareP *) + keyshares, + keyshare_length, + &salt, + &policy_keys[i]); + if (i > 0) + GNUNET_assert (0 != + GNUNET_memcmp (&policy_keys[i - 1], &policy_keys[i])); + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "CORE_SECRET_BEFORE: %s\n", + TALER_b2s (test, strlen (test))); + + // test encryption of core_secret + ANASTASIS_CRYPTO_core_secret_encrypt (policy_keys, + policy_keys_length, + test, + strlen (test), + &enc_core_secret, + (struct + ANASTASIS_CRYPTO_EncryptedMasterKeyP *) + &encrypted_master_keys); + + // test recover of core secret + for (unsigned int k = 0; k < policy_keys_length; k++) + { + void *dec_core_secret; + size_t core_secret_size; + + ANASTASIS_CRYPTO_core_secret_recover (&encrypted_master_keys[k], + &policy_keys[k], + enc_core_secret, + strlen (test), + &dec_core_secret, + &core_secret_size); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "CORE_SECRET_AFTER_%i: %s\n", + k, + TALER_b2s (dec_core_secret, strlen (test))); + GNUNET_assert (strlen (test) == core_secret_size); + GNUNET_assert (0 == + strncmp (dec_core_secret, test, strlen (test))); + GNUNET_assert (0 != + strncmp (dec_core_secret, test_wrong, strlen ( + test))); + GNUNET_free (dec_core_secret); + } + GNUNET_free (enc_core_secret); + return 0; +} + + +static int +test_public_key_derive (void) +{ + struct ANASTASIS_CRYPTO_UserIdentifierP id; + struct ANASTASIS_CRYPTO_AccountPublicKeyP pub_key; + struct ANASTASIS_CRYPTO_ProviderSaltP server_salt; + json_t *id_data = json_object (); + const char *salt_str = "Server-Salt-Test"; + + GNUNET_memcpy (&server_salt, + salt_str, + strlen (salt_str)); + + json_object_set_new (id_data, "arg1", json_string ("ID_DATA")); + ANASTASIS_CRYPTO_user_identifier_derive (id_data, + &server_salt, + &id); + + ANASTASIS_CRYPTO_account_public_key_derive (&id, + &pub_key); + // FIXME: write a real test, e.g. signing and verification + json_decref (id_data); + return 0; +} + + +int +main (int argc, + const char *const argv[]) +{ + GNUNET_log_setup (argv[0], "DEBUG", NULL); + if (0 != test_recovery_document ()) + return 1; + if (0 != test_user_identifier_derive ()) + return 1; + if (0 != test_key_share ()) + return 1; + if (0 != test_truth ()) + return 1; + if (0 != test_core_secret ()) + return 1; + if (0 != test_public_key_derive ()) + return 1; + return 0; +} + + +/* end of test_anastasis_crypto.c */ -- cgit v1.2.3