diff options
Diffstat (limited to 'src/kyclogic')
-rw-r--r-- | src/kyclogic/Makefile.am | 144 | ||||
-rw-r--r-- | src/kyclogic/kyclogic-kycaid.conf | 26 | ||||
-rw-r--r-- | src/kyclogic/kyclogic-oauth2.conf | 35 | ||||
-rw-r--r-- | src/kyclogic/kyclogic-persona.conf | 44 | ||||
-rw-r--r-- | src/kyclogic/kyclogic.conf | 15 | ||||
-rw-r--r-- | src/kyclogic/kyclogic_api.c | 1512 | ||||
-rw-r--r-- | src/kyclogic/plugin_kyclogic_kycaid.c | 1480 | ||||
-rw-r--r-- | src/kyclogic/plugin_kyclogic_oauth2.c | 1780 | ||||
-rw-r--r-- | src/kyclogic/plugin_kyclogic_persona.c | 2268 | ||||
-rw-r--r-- | src/kyclogic/plugin_kyclogic_template.c | 468 | ||||
-rw-r--r-- | src/kyclogic/sample.conf | 33 | ||||
-rwxr-xr-x | src/kyclogic/taler-exchange-kyc-kycaid-converter.sh | 90 | ||||
-rwxr-xr-x | src/kyclogic/taler-exchange-kyc-oauth2-challenger.sh | 27 | ||||
-rwxr-xr-x | src/kyclogic/taler-exchange-kyc-oauth2-nda.sh | 30 | ||||
-rwxr-xr-x | src/kyclogic/taler-exchange-kyc-oauth2-test-converter.sh | 31 | ||||
-rwxr-xr-x | src/kyclogic/taler-exchange-kyc-persona-converter.sh | 57 | ||||
-rw-r--r-- | src/kyclogic/taler-exchange-kyc-tester.c | 1646 |
17 files changed, 9686 insertions, 0 deletions
diff --git a/src/kyclogic/Makefile.am b/src/kyclogic/Makefile.am new file mode 100644 index 000000000..0281553fc --- /dev/null +++ b/src/kyclogic/Makefile.am @@ -0,0 +1,144 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +pkgcfgdir = $(prefix)/share/taler/config.d/ + +pkgcfg_DATA = \ + kyclogic.conf \ + kyclogic-kycaid.conf \ + kyclogic-oauth2.conf \ + kyclogic-persona.conf + +bin_SCRIPTS = \ + taler-exchange-kyc-kycaid-converter.sh \ + taler-exchange-kyc-persona-converter.sh \ + taler-exchange-kyc-oauth2-test-converter.sh \ + taler-exchange-kyc-oauth2-challenger.sh \ + taler-exchange-kyc-oauth2-nda.sh + +EXTRA_DIST = \ + $(pkgcfg_DATA) \ + $(bin_SCRIPTS) \ + sample.conf + +lib_LTLIBRARIES = \ + libtalerkyclogic.la + +libtalerkyclogic_la_SOURCES = \ + kyclogic_api.c +libtalerkyclogic_la_LIBADD = \ + $(top_builddir)/src/util/libtalerutil.la \ + -ljansson \ + -lgnunetutil \ + $(XLIB) +libtalerkyclogic_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined + + +bin_PROGRAMS = \ + taler-exchange-kyc-tester + +taler_exchange_kyc_tester_SOURCES = \ + taler-exchange-kyc-tester.c +taler_exchange_kyc_tester_LDADD = \ + $(LIBGCRYPT_LIBS) \ + libtalerkyclogic.la \ + $(top_builddir)/src/mhd/libtalermhd.la \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/templating/libtalertemplating.la \ + $(top_builddir)/src/util/libtalerutil.la \ + -lmicrohttpd \ + -lgnunetcurl \ + -lgnunetutil \ + -lgnunetjson \ + -ljansson \ + -lcurl \ + -lz \ + $(XLIB) + + + +plugindir = $(libdir)/taler + +plugin_LTLIBRARIES = \ + libtaler_plugin_kyclogic_kycaid.la \ + libtaler_plugin_kyclogic_oauth2.la \ + libtaler_plugin_kyclogic_persona.la \ + libtaler_plugin_kyclogic_template.la + +libtaler_plugin_kyclogic_template_la_SOURCES = \ + plugin_kyclogic_template.c +libtaler_plugin_kyclogic_template_la_LIBADD = \ + $(LTLIBINTL) +libtaler_plugin_kyclogic_template_la_LDFLAGS = \ + $(TALER_PLUGIN_LDFLAGS) \ + -lgnunetcurl \ + -lgnunetutil \ + $(XLIB) + +libtaler_plugin_kyclogic_oauth2_la_SOURCES = \ + plugin_kyclogic_oauth2.c +libtaler_plugin_kyclogic_oauth2_la_LIBADD = \ + $(LTLIBINTL) +libtaler_plugin_kyclogic_oauth2_la_LDFLAGS = \ + $(TALER_PLUGIN_LDFLAGS) \ + $(top_builddir)/src/templating/libtalertemplating.la \ + $(top_builddir)/src/mhd/libtalermhd.la \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/util/libtalerutil.la \ + -lgnunetcurl \ + -lgnunetjson \ + -lgnunetutil \ + -lmicrohttpd \ + -ljansson \ + -lcurl \ + $(XLIB) + +libtaler_plugin_kyclogic_kycaid_la_SOURCES = \ + plugin_kyclogic_kycaid.c +libtaler_plugin_kyclogic_kycaid_la_LIBADD = \ + $(LTLIBINTL) +libtaler_plugin_kyclogic_kycaid_la_LDFLAGS = \ + $(TALER_PLUGIN_LDFLAGS) \ + $(top_builddir)/src/templating/libtalertemplating.la \ + $(top_builddir)/src/mhd/libtalermhd.la \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/curl/libtalercurl.la \ + $(top_builddir)/src/util/libtalerutil.la \ + -lgnunetcurl \ + -lgnunetjson \ + -lgnunetutil \ + -lmicrohttpd \ + -ljansson \ + -lcurl \ + $(XLIB) + +libtaler_plugin_kyclogic_persona_la_SOURCES = \ + plugin_kyclogic_persona.c +libtaler_plugin_kyclogic_persona_la_LIBADD = \ + $(LTLIBINTL) +libtaler_plugin_kyclogic_persona_la_DEPENDENCIES = \ + libtalerkyclogic.la +libtaler_plugin_kyclogic_persona_la_LDFLAGS = \ + $(TALER_PLUGIN_LDFLAGS) \ + libtalerkyclogic.la \ + $(top_builddir)/src/mhd/libtalermhd.la \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/curl/libtalercurl.la \ + $(top_builddir)/src/templating/libtalertemplating.la \ + $(top_builddir)/src/util/libtalerutil.la \ + -lgnunetcurl \ + -lgnunetjson \ + -lgnunetutil \ + -lmicrohttpd \ + -ljansson \ + -lcurl \ + $(XLIB) + +AM_TESTS_ENVIRONMENT=export TALER_PREFIX=$${TALER_PREFIX:-@libdir@};export PATH=$${TALER_PREFIX:-@prefix@}/bin:$$PATH; diff --git a/src/kyclogic/kyclogic-kycaid.conf b/src/kyclogic/kyclogic-kycaid.conf new file mode 100644 index 000000000..753fb689d --- /dev/null +++ b/src/kyclogic/kyclogic-kycaid.conf @@ -0,0 +1,26 @@ +# This file is in the public domain. + +# Example kycaid provider configuration. + +[kyc-provider-example-kycaid] + +COST = 42 +LOGIC = kycaid +USER_TYPE = INDIVIDUAL +PROVIDED_CHECKS = EXAMPLE_DO_NOT_USE + +# How long is the KYC check valid? +KYC_KYCAID_VALIDITY = forever + +# Program that converts Persona KYC data into the +# GNU Taler format. +KYC_KYCAID_CONVERTER_HELPER = taler-exchange-kyc-kycaid-converter.sh + +# Authentication token to use. +KYC_KYCAID_AUTH_TOKEN = XXX + +# Form to use. +KYC_KYCAID_FORM_ID = XXX + +# URL to go to after the process is complete. +KYC_KYCAID_POST_URL = https://example.com/ diff --git a/src/kyclogic/kyclogic-oauth2.conf b/src/kyclogic/kyclogic-oauth2.conf new file mode 100644 index 000000000..57e1fc13a --- /dev/null +++ b/src/kyclogic/kyclogic-oauth2.conf @@ -0,0 +1,35 @@ +# This file is in the public domain. + +# Example Oauth2.0 provider configuration. + +[kyc-provider-example-oauth2] + +COST = 42 +LOGIC = oauth2 +USER_TYPE = INDIVIDUAL +PROVIDED_CHECKS = EXAMPLE_DO_NOT_USE + +# How long is the KYC check valid? +KYC_OAUTH2_VALIDITY = forever + +# URL where we initiate the user's login process +KYC_OAUTH2_AUTHORIZE_URL = https://kyc.example.com/authorize +# URL where we send the user's authentication information +KYC_OAUTH2_TOKEN_URL = https://kyc.example.com/token +# URL of the user info access point. +KYC_OAUTH2_INFO_URL = https://kyc.example.com/info + +# Where does the client get redirected upon completion? +KYC_OAUTH2_POST_URL = http://example.com/thank-you + +# For authentication to the OAuth2.0 service +KYC_OAUTH2_CLIENT_ID = testcase +KYC_OAUTH2_CLIENT_SECRET = password + +# Mustach template that converts OAuth2.0 data about the user +# into GNU Taler standardized attribute data. +# +# This is just an example, you need to pick the right converter +# for the provider! +# +KYC_OAUTH2_CONVERTER_HELPER = taler-exchange-kyc-oauth2-converter.sh diff --git a/src/kyclogic/kyclogic-persona.conf b/src/kyclogic/kyclogic-persona.conf new file mode 100644 index 000000000..2d52a9ee0 --- /dev/null +++ b/src/kyclogic/kyclogic-persona.conf @@ -0,0 +1,44 @@ +# This file is in the public domain. + +# FIXME: add to taler.conf man page! + +# Example persona provider configuration. + +[kyclogic-persona] + +# Optional authorization token for the webhook. +# This must be the same for all uses of the +# Persona provider, and is thus not in a +# template-specific section. +#WEBHOOK_AUTH_TOKEN = wbhsec_698b5a19-c790-47f6-b396-deb572ec82f9 + + +[kyc-provider-example-persona] + +COST = 42 +LOGIC = persona +USER_TYPE = INDIVIDUAL +PROVIDED_CHECKS = EXAMPLE_DO_NOT_USE + +# How long is the KYC check valid? +KYC_PERSONA_VALIDITY = forever + +# Which subdomain is used for our API? +KYC_PERSONA_SUBDOMAIN = taler + +# Authentication token to use. +KYC_PERSONA_AUTH_TOKEN = persona_sandbox_42 + +# Program that converts Persona KYC data into the +# GNU Taler format. +KYC_PERSONA_CONVERTER_HELPER = taler-exchange-kyc-persona-converter.sh + +# Form to use. +KYC_PERSONA_TEMPLATE_ID = itempl_Uj6Xxxxx + +# Where do we redirect to after KYC finished successfully. +KYC_PERSONA_POST_URL = https://taler.net/ + +# Salt to give to requests for idempotency. +# Optional. +# KYC_PERSONA_SALT = salt
\ No newline at end of file diff --git a/src/kyclogic/kyclogic.conf b/src/kyclogic/kyclogic.conf new file mode 100644 index 000000000..eca3b24c2 --- /dev/null +++ b/src/kyclogic/kyclogic.conf @@ -0,0 +1,15 @@ +# This file is in the public domain. +# +# Sample legitimization rule set. + +#[kyc-legitimization-withdraw-high] +# KYC hook is this rule is about. +#OPERATION_TYPE = WITHDRAW +# Which checks must be done. Give names used by providers. +#REQUIRED_CHECKS = PHONE GOVID SSN +# Threshold amount above which the checks are required. +#THRESHOLD = KUDOS:100 +# Timeframe over which amounts involved in the +# operation type are accumulated to test against +# the threshold. +#TIMEFRAME = 1a diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c new file mode 100644 index 000000000..186799dbb --- /dev/null +++ b/src/kyclogic/kyclogic_api.c @@ -0,0 +1,1512 @@ +/* + This file is part of TALER + Copyright (C) 2022-2023 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file kyclogic_api.c + * @brief server-side KYC API + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_kyclogic_lib.h" + +/** + * Name of the KYC check that may never be passed. Useful if some + * operations/amounts are categorically forbidden. + */ +#define KYC_CHECK_IMPOSSIBLE "impossible" + +/** + * Information about a KYC provider. + */ +struct TALER_KYCLOGIC_KycProvider; + + +/** + * Abstract representation of a KYC check. + */ +struct TALER_KYCLOGIC_KycCheck +{ + /** + * Human-readable name given to the KYC check. + */ + char *name; + + /** + * Array of @e num_providers providers that offer this type of KYC check. + */ + struct TALER_KYCLOGIC_KycProvider **providers; + + /** + * Length of the @e providers array. + */ + unsigned int num_providers; + +}; + + +struct TALER_KYCLOGIC_KycProvider +{ + /** + * Name of the provider (configuration section name). + */ + const char *provider_section_name; + + /** + * Array of @e num_checks checks performed by this provider. + */ + struct TALER_KYCLOGIC_KycCheck **provided_checks; + + /** + * Logic to run for this provider. + */ + struct TALER_KYCLOGIC_Plugin *logic; + + /** + * @e provider_section_name specific details to + * pass to the @e logic functions. + */ + struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Cost of running this provider's KYC. + */ + unsigned long long cost; + + /** + * Length of the @e checks array. + */ + unsigned int num_checks; + + /** + * Type of user this provider supports. + */ + enum TALER_KYCLOGIC_KycUserType user_type; +}; + + +/** + * Condition that triggers a need to perform KYC. + */ +struct TALER_KYCLOGIC_KycTrigger +{ + + /** + * Timeframe to consider for computing the amount + * to compare against the @e limit. Zero for the + * wallet balance trigger (as not applicable). + */ + struct GNUNET_TIME_Relative timeframe; + + /** + * Maximum amount that can be transacted until + * the rule triggers. + */ + struct TALER_Amount threshold; + + /** + * Array of @e num_checks checks to apply on this trigger. + */ + struct TALER_KYCLOGIC_KycCheck **required_checks; + + /** + * Length of the @e checks array. + */ + unsigned int num_checks; + + /** + * What event is this trigger for? + */ + enum TALER_KYCLOGIC_KycTriggerEvent trigger; + +}; + + +/** + * Array of @e num_kyc_logics KYC logic plugins we have loaded. + */ +static struct TALER_KYCLOGIC_Plugin **kyc_logics; + +/** + * Length of the #kyc_logics array. + */ +static unsigned int num_kyc_logics; + +/** + * Array of @e num_kyc_checks known types of + * KYC checks. + */ +static struct TALER_KYCLOGIC_KycCheck **kyc_checks; + +/** + * Length of the #kyc_checks array. + */ +static unsigned int num_kyc_checks; + +/** + * Array of configured triggers. + */ +static struct TALER_KYCLOGIC_KycTrigger **kyc_triggers; + +/** + * Length of the #kyc_triggers array. + */ +static unsigned int num_kyc_triggers; + +/** + * Array of configured providers. + */ +static struct TALER_KYCLOGIC_KycProvider **kyc_providers; + +/** + * Length of the #kyc_providers array. + */ +static unsigned int num_kyc_providers; + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_kyc_trigger_from_string (const char *trigger_s, + enum TALER_KYCLOGIC_KycTriggerEvent * + trigger) +{ + struct + { + const char *in; + enum TALER_KYCLOGIC_KycTriggerEvent out; + } map [] = { + { "withdraw", TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW }, + { "age-withdraw", TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW }, + { "deposit", TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT }, + { "merge", TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE }, + { "balance", TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE }, + { "close", TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE }, + { NULL, 0 } + }; + + for (unsigned int i = 0; NULL != map[i].in; i++) + if (0 == strcasecmp (map[i].in, + trigger_s)) + { + *trigger = map[i].out; + return GNUNET_OK; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid KYC trigger `%s'\n", + trigger_s); + return GNUNET_SYSERR; +} + + +const char * +TALER_KYCLOGIC_kyc_trigger2s (enum TALER_KYCLOGIC_KycTriggerEvent trigger) +{ + switch (trigger) + { + case TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW: + return "withdraw"; + case TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW: + return "age-withdraw"; + case TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT: + return "deposit"; + case TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE: + return "merge"; + case TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE: + return "balance"; + case TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE: + return "close"; + } + GNUNET_break (0); + return NULL; +} + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_kyc_user_type_from_string (const char *ut_s, + enum TALER_KYCLOGIC_KycUserType *ut) +{ + struct + { + const char *in; + enum TALER_KYCLOGIC_KycUserType out; + } map [] = { + { "individual", TALER_KYCLOGIC_KYC_UT_INDIVIDUAL }, + { "business", TALER_KYCLOGIC_KYC_UT_BUSINESS }, + { NULL, 0 } + }; + + for (unsigned int i = 0; NULL != map[i].in; i++) + if (0 == strcasecmp (map[i].in, + ut_s)) + { + *ut = map[i].out; + return GNUNET_OK; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid user type `%s'\n", + ut_s); + return GNUNET_SYSERR; +} + + +const char * +TALER_KYCLOGIC_kyc_user_type2s (enum TALER_KYCLOGIC_KycUserType ut) +{ + switch (ut) + { + case TALER_KYCLOGIC_KYC_UT_INDIVIDUAL: + return "individual"; + case TALER_KYCLOGIC_KYC_UT_BUSINESS: + return "business"; + } + GNUNET_break (0); + return NULL; +} + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_check_satisfiable ( + const char *check_name) +{ + for (unsigned int i = 0; i<num_kyc_checks; i++) + if (0 == strcmp (check_name, + kyc_checks[i]->name)) + return GNUNET_OK; + if (0 == strcmp (check_name, + KYC_CHECK_IMPOSSIBLE)) + return GNUNET_NO; + return GNUNET_SYSERR; +} + + +json_t * +TALER_KYCLOGIC_get_satisfiable () +{ + json_t *requirements; + + requirements = json_array (); + GNUNET_assert (NULL != requirements); + for (unsigned int i = 0; i<num_kyc_checks; i++) + GNUNET_assert ( + 0 == + json_array_append_new ( + requirements, + json_string (kyc_checks[i]->name))); + return requirements; +} + + +/** + * Load KYC logic plugin. + * + * @param cfg configuration to use + * @param name name of the plugin + * @return NULL on error + */ +static struct TALER_KYCLOGIC_Plugin * +load_logic (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *name) +{ + char *lib_name; + struct TALER_KYCLOGIC_Plugin *plugin; + + GNUNET_asprintf (&lib_name, + "libtaler_plugin_kyclogic_%s", + name); + for (unsigned int i = 0; i<num_kyc_logics; i++) + if (0 == strcmp (lib_name, + kyc_logics[i]->library_name)) + { + GNUNET_free (lib_name); + return kyc_logics[i]; + } + plugin = GNUNET_PLUGIN_load (lib_name, + (void *) cfg); + if (NULL == plugin) + { + GNUNET_free (lib_name); + return NULL; + } + plugin->library_name = lib_name; + plugin->name = GNUNET_strdup (name); + GNUNET_array_append (kyc_logics, + num_kyc_logics, + plugin); + return plugin; +} + + +/** + * Add check type to global array of checks. First checks if the type already + * exists, otherwise adds a new one. + * + * @param check name of the check + * @return pointer into the global list + */ +static struct TALER_KYCLOGIC_KycCheck * +add_check (const char *check) +{ + struct TALER_KYCLOGIC_KycCheck *kc; + + for (unsigned int i = 0; i<num_kyc_checks; i++) + if (0 == strcasecmp (check, + kyc_checks[i]->name)) + return kyc_checks[i]; + kc = GNUNET_new (struct TALER_KYCLOGIC_KycCheck); + kc->name = GNUNET_strdup (check); + GNUNET_array_append (kyc_checks, + num_kyc_checks, + kc); + return kc; +} + + +/** + * Parse list of checks from @a checks and build an array of aliases into the + * global checks array in @a provided_checks. + * + * @param[in,out] checks list of checks; clobbered + * @param[out] p_checks where to put array of aliases + * @param[out] num_p_checks set to length of @a p_checks array + */ +static void +add_checks (char *checks, + struct TALER_KYCLOGIC_KycCheck ***p_checks, + unsigned int *num_p_checks) +{ + char *sptr; + struct TALER_KYCLOGIC_KycCheck **rchecks = NULL; + unsigned int num_rchecks = 0; + + for (char *tok = strtok_r (checks, " ", &sptr); + NULL != tok; + tok = strtok_r (NULL, " ", &sptr)) + { + struct TALER_KYCLOGIC_KycCheck *kc; + + kc = add_check (tok); + GNUNET_array_append (rchecks, + num_rchecks, + kc); + } + *p_checks = rchecks; + *num_p_checks = num_rchecks; +} + + +/** + * Parse configuration of a KYC provider. + * + * @param cfg configuration to parse + * @param section name of the section to analyze + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +add_provider (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section) +{ + unsigned long long cost; + char *logic; + char *ut_s; + enum TALER_KYCLOGIC_KycUserType ut; + char *checks; + struct TALER_KYCLOGIC_Plugin *lp; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (cfg, + section, + "COST", + &cost)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "COST", + "number required"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "USER_TYPE", + &ut_s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "USER_TYPE"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_user_type_from_string (ut_s, + &ut)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "USER_TYPE", + "valid user type required"); + GNUNET_free (ut_s); + return GNUNET_SYSERR; + } + GNUNET_free (ut_s); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "LOGIC", + &logic)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "LOGIC"); + return GNUNET_SYSERR; + } + lp = load_logic (cfg, + logic); + if (NULL == lp) + { + GNUNET_free (logic); + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "LOGIC", + "logic plugin could not be loaded"); + return GNUNET_SYSERR; + } + GNUNET_free (logic); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "PROVIDED_CHECKS", + &checks)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "PROVIDED_CHECKS"); + return GNUNET_SYSERR; + } + { + struct TALER_KYCLOGIC_KycProvider *kp; + + kp = GNUNET_new (struct TALER_KYCLOGIC_KycProvider); + kp->provider_section_name = section; + kp->user_type = ut; + kp->logic = lp; + kp->cost = cost; + add_checks (checks, + &kp->provided_checks, + &kp->num_checks); + GNUNET_free (checks); + kp->pd = lp->load_configuration (lp->cls, + section); + if (NULL == kp->pd) + { + GNUNET_free (kp); + return GNUNET_SYSERR; + } + GNUNET_array_append (kyc_providers, + num_kyc_providers, + kp); + for (unsigned int i = 0; i<kp->num_checks; i++) + { + struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[i]; + + GNUNET_array_append (kc->providers, + kc->num_providers, + kp); + } + } + return GNUNET_OK; +} + + +/** + * Parse configuration @a cfg in section @a section for + * the specification of a KYC trigger. + * + * @param cfg configuration to parse + * @param section configuration section to parse + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +add_trigger (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section) +{ + char *ot_s; + struct TALER_Amount threshold; + struct GNUNET_TIME_Relative timeframe; + char *checks; + enum TALER_KYCLOGIC_KycTriggerEvent ot; + + if (GNUNET_OK != + TALER_config_get_amount (cfg, + section, + "THRESHOLD", + &threshold)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "THRESHOLD", + "amount required"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "OPERATION_TYPE", + &ot_s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "OPERATION_TYPE"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_trigger_from_string (ot_s, + &ot)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "OPERATION_TYPE", + "valid trigger type required"); + GNUNET_free (ot_s); + return GNUNET_SYSERR; + } + GNUNET_free (ot_s); + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (cfg, + section, + "TIMEFRAME", + &timeframe)) + { + if (TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE == ot) + { + timeframe = GNUNET_TIME_UNIT_ZERO; + } + else + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "TIMEFRAME", + "duration required"); + return GNUNET_SYSERR; + } + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "REQUIRED_CHECKS", + &checks)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "REQUIRED_CHECKS"); + return GNUNET_SYSERR; + } + + { + struct TALER_KYCLOGIC_KycTrigger *kt; + + kt = GNUNET_new (struct TALER_KYCLOGIC_KycTrigger); + kt->timeframe = timeframe; + kt->threshold = threshold; + kt->trigger = ot; + add_checks (checks, + &kt->required_checks, + &kt->num_checks); + GNUNET_free (checks); + GNUNET_array_append (kyc_triggers, + num_kyc_triggers, + kt); + for (unsigned int i = 0; i<kt->num_checks; i++) + { + const struct TALER_KYCLOGIC_KycCheck *ck = kt->required_checks[i]; + + if (0 != ck->num_providers) + continue; + if (0 == strcmp (ck->name, + KYC_CHECK_IMPOSSIBLE)) + continue; + { + char *msg; + + GNUNET_asprintf (&msg, + "Required check `%s' cannot be satisfied: not provided by any provider", + ck->name); + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "REQUIRED_CHECKS", + msg); + GNUNET_free (msg); + } + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * Closure for #handle_section(). + */ +struct SectionContext +{ + /** + * Configuration to handle. + */ + const struct GNUNET_CONFIGURATION_Handle *cfg; + + /** + * Result to return, set to false on failures. + */ + bool result; +}; + + +/** + * Function to iterate over configuration sections. + * + * @param cls a `struct SectionContext *` + * @param section name of the section + */ +static void +handle_provider_section (void *cls, + const char *section) +{ + struct SectionContext *sc = cls; + + if (0 == strncasecmp (section, + "kyc-provider-", + strlen ("kyc-provider-"))) + { + if (GNUNET_OK != + add_provider (sc->cfg, + section)) + sc->result = false; + return; + } +} + + +/** + * Function to iterate over configuration sections. + * + * @param cls a `struct SectionContext *` + * @param section name of the section + */ +static void +handle_trigger_section (void *cls, + const char *section) +{ + struct SectionContext *sc = cls; + + if (0 == strncasecmp (section, + "kyc-legitimization-", + strlen ("kyc-legitimization-"))) + { + if (GNUNET_OK != + add_trigger (sc->cfg, + section)) + sc->result = false; + return; + } +} + + +/** + * Comparator for qsort. Compares two triggers + * by timeframe to sort triggers by time. + * + * @param p1 first trigger to compare + * @param p2 second trigger to compare + * @return -1 if p1 < p2, 0 if p1==p2, 1 if p1 > p2. + */ +static int +sort_by_timeframe (const void *p1, + const void *p2) +{ + struct TALER_KYCLOGIC_KycTrigger **t1 = (struct + TALER_KYCLOGIC_KycTrigger **) p1; + struct TALER_KYCLOGIC_KycTrigger **t2 = (struct + TALER_KYCLOGIC_KycTrigger **) p2; + + if (GNUNET_TIME_relative_cmp ((*t1)->timeframe, + <, + (*t2)->timeframe)) + return -1; + if (GNUNET_TIME_relative_cmp ((*t1)->timeframe, + >, + (*t2)->timeframe)) + return 1; + return 0; +} + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_kyc_init (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + struct SectionContext sc = { + .cfg = cfg, + .result = true + }; + + GNUNET_CONFIGURATION_iterate_sections (cfg, + &handle_provider_section, + &sc); + GNUNET_CONFIGURATION_iterate_sections (cfg, + &handle_trigger_section, + &sc); + if (! sc.result) + { + TALER_KYCLOGIC_kyc_done (); + return GNUNET_SYSERR; + } + + /* sanity check: ensure at least one provider exists + for any trigger and indidivual or business. */ + for (unsigned int i = 0; i<num_kyc_checks; i++) + if (0 == kyc_checks[i]->num_providers) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No provider available for required KYC check `%s'\n", + kyc_checks[i]->name); + TALER_KYCLOGIC_kyc_done (); + return GNUNET_SYSERR; + } + if (0 != num_kyc_triggers) + qsort (kyc_triggers, + num_kyc_triggers, + sizeof (struct TALER_KYCLOGIC_KycTrigger *), + &sort_by_timeframe); + return GNUNET_OK; +} + + +void +TALER_KYCLOGIC_kyc_done (void) +{ + for (unsigned int i = 0; i<num_kyc_triggers; i++) + { + struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; + + GNUNET_array_grow (kt->required_checks, + kt->num_checks, + 0); + GNUNET_free (kt); + } + GNUNET_array_grow (kyc_triggers, + num_kyc_triggers, + 0); + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + kp->logic->unload_configuration (kp->pd); + GNUNET_array_grow (kp->provided_checks, + kp->num_checks, + 0); + GNUNET_free (kp); + } + GNUNET_array_grow (kyc_providers, + num_kyc_providers, + 0); + for (unsigned int i = 0; i<num_kyc_logics; i++) + { + struct TALER_KYCLOGIC_Plugin *lp = kyc_logics[i]; + char *lib_name = lp->library_name; + + GNUNET_free (lp->name); + GNUNET_assert (NULL == GNUNET_PLUGIN_unload (lib_name, + lp)); + GNUNET_free (lib_name); + } + GNUNET_array_grow (kyc_logics, + num_kyc_logics, + 0); + for (unsigned int i = 0; i<num_kyc_checks; i++) + { + struct TALER_KYCLOGIC_KycCheck *kc = kyc_checks[i]; + + GNUNET_array_grow (kc->providers, + kc->num_providers, + 0); + GNUNET_free (kc->name); + GNUNET_free (kc); + } + GNUNET_array_grow (kyc_checks, + num_kyc_checks, + 0); +} + + +/** + * Closure for the #eval_trigger(). + */ +struct ThresholdTestContext +{ + /** + * Total amount so far. + */ + struct TALER_Amount total; + + /** + * Trigger event to evaluate triggers of. + */ + enum TALER_KYCLOGIC_KycTriggerEvent event; + + /** + * Offset in the triggers array where we need to start + * checking for triggers. All trigges below this + * offset were already hit. + */ + unsigned int start; + + /** + * Array of checks needed so far. + */ + struct TALER_KYCLOGIC_KycCheck **needed; + + /** + * Pointer to number of entries used in @a needed. + */ + unsigned int *needed_cnt; + + /** + * Has @e total been initialized yet? + */ + bool have_total; +}; + + +/** + * Function called on each @a amount that was found to + * be relevant for a KYC check. + * + * @param cls closure to allow the KYC module to + * total up amounts and evaluate rules + * @param amount encountered transaction amount + * @param date when was the amount encountered + * @return #GNUNET_OK to continue to iterate, + * #GNUNET_NO to abort iteration + * #GNUNET_SYSERR on internal error (also abort itaration) + */ +static enum GNUNET_GenericReturnValue +eval_trigger (void *cls, + const struct TALER_Amount *amount, + struct GNUNET_TIME_Absolute date) +{ + struct ThresholdTestContext *ttc = cls; + struct GNUNET_TIME_Relative duration; + bool bump = true; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check with new amount %s\n", + TALER_amount2s (amount)); + duration = GNUNET_TIME_absolute_get_duration (date); + if (ttc->have_total) + { + if (0 > + TALER_amount_add (&ttc->total, + &ttc->total, + amount)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + } + else + { + ttc->total = *amount; + ttc->have_total = true; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check: new total is %s\n", + TALER_amount2s (&ttc->total)); + for (unsigned int i = ttc->start; i<num_kyc_triggers; i++) + { + const struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; + + if (ttc->event != kt->trigger) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check #%u: trigger type does not match\n", + i); + continue; + } + duration = GNUNET_TIME_relative_max (duration, + kt->timeframe); + if (GNUNET_TIME_relative_cmp (kt->timeframe, + >, + duration)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check #%u: amount is beyond time limit\n", + i); + if (bump) + ttc->start = i; + return GNUNET_OK; + } + if (-1 == + TALER_amount_cmp (&ttc->total, + &kt->threshold)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check #%u: amount is below threshold\n", + i); + if (bump) + ttc->start = i; + bump = false; + continue; /* amount too low to trigger */ + } + /* add check to list of required checks, unless + already present... */ + for (unsigned int j = 0; j<kt->num_checks; j++) + { + struct TALER_KYCLOGIC_KycCheck *rc = kt->required_checks[j]; + bool found = false; + + for (unsigned int k = 0; k<*ttc->needed_cnt; k++) + if (ttc->needed[k] == rc) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC rule #%u already listed\n", + j); + found = true; + break; + } + if (! found) + { + ttc->needed[*ttc->needed_cnt] = rc; + (*ttc->needed_cnt)++; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC check #%u (%s) is applicable, %u checks needed so far\n", + i, + ttc->needed[(*ttc->needed_cnt) - 1]->name, + *ttc->needed_cnt); + } + if (bump) + return GNUNET_NO; /* we hit all possible triggers! */ + return GNUNET_OK; +} + + +/** + * Closure for the #remove_satisfied(). + */ +struct RemoveContext +{ + + /** + * Array of checks needed so far. + */ + struct TALER_KYCLOGIC_KycCheck **needed; + + /** + * Pointer to number of entries used in @a needed. + */ + unsigned int *needed_cnt; + + /** + * Object with information about collected KYC data. + */ + json_t *kyc_details; +}; + + +/** + * Remove all checks satisfied by @a provider_name from + * our list of checks. + * + * @param cls a `struct RemoveContext` + * @param provider_name section name of provider that was already run previously + */ +static void +remove_satisfied (void *cls, + const char *provider_name) +{ + struct RemoveContext *rc = cls; + + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + const struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + if (0 != strcasecmp (provider_name, + kp->provider_section_name)) + continue; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Provider `%s' satisfied\n", + provider_name); + for (unsigned int j = 0; j<kp->num_checks; j++) + { + const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Provider satisfies check `%s'\n", + kc->name); + if (NULL != rc->kyc_details) + { + GNUNET_assert (0 == + json_object_set_new ( + rc->kyc_details, + kc->name, + json_object ())); + } + for (unsigned int k = 0; k<*rc->needed_cnt; k++) + if (kc == rc->needed[k]) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Removing check `%s' from list\n", + kc->name); + rc->needed[k] = rc->needed[*rc->needed_cnt - 1]; + (*rc->needed_cnt)--; + if (0 == *rc->needed_cnt) + return; /* for sure finished */ + break; + } + } + break; + } +} + + +enum GNUNET_DB_QueryStatus +TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, + const struct TALER_PaytoHashP *h_payto, + TALER_KYCLOGIC_KycSatisfiedIterator ki, + void *ki_cls, + TALER_KYCLOGIC_KycAmountIterator ai, + void *ai_cls, + char **required) +{ + struct TALER_KYCLOGIC_KycCheck *needed[num_kyc_checks]; + unsigned int needed_cnt = 0; + char *ret; + struct GNUNET_TIME_Relative timeframe; + + timeframe = GNUNET_TIME_UNIT_ZERO; + for (unsigned int i = 0; i<num_kyc_triggers; i++) + { + const struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; + + if (event != kt->trigger) + continue; + timeframe = GNUNET_TIME_relative_max (timeframe, + kt->timeframe); + } + { + struct GNUNET_TIME_Absolute now; + struct ThresholdTestContext ttc = { + .event = event, + .needed = needed, + .needed_cnt = &needed_cnt + }; + + now = GNUNET_TIME_absolute_get (); + ai (ai_cls, + GNUNET_TIME_absolute_subtract (now, + timeframe), + &eval_trigger, + &ttc); + } + if (0 == needed_cnt) + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + timeframe = GNUNET_TIME_UNIT_ZERO; + for (unsigned int i = 0; i<num_kyc_triggers; i++) + { + const struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; + + if (event != kt->trigger) + continue; + timeframe = GNUNET_TIME_relative_max (timeframe, + kt->timeframe); + } + { + struct GNUNET_TIME_Absolute now; + struct ThresholdTestContext ttc = { + .event = event, + .needed = needed, + .needed_cnt = &needed_cnt + }; + + now = GNUNET_TIME_absolute_get (); + ai (ai_cls, + GNUNET_TIME_absolute_subtract (now, + timeframe), + &eval_trigger, + &ttc); + } + if (0 == needed_cnt) + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + { + struct RemoveContext rc = { + .needed = needed, + .needed_cnt = &needed_cnt + }; + enum GNUNET_DB_QueryStatus qs; + + /* Check what provider checks are already satisfied for h_payto (with + database), remove those from the 'needed' array. */ + qs = ki (ki_cls, + h_payto, + &remove_satisfied, + &rc); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + } + if (0 == needed_cnt) + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + { + struct RemoveContext rc = { + .needed = needed, + .needed_cnt = &needed_cnt + }; + enum GNUNET_DB_QueryStatus qs; + + /* Check what provider checks are already satisfied for h_payto (with + database), remove those from the 'needed' array. */ + qs = ki (ki_cls, + h_payto, + &remove_satisfied, + &rc); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + } + if (0 == needed_cnt) + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + ret = NULL; + for (unsigned int k = 0; k<needed_cnt; k++) + { + const struct TALER_KYCLOGIC_KycCheck *kc = needed[k]; + + if (NULL == ret) + { + ret = GNUNET_strdup (kc->name); + } + else /* append */ + { + char *tmp = ret; + + GNUNET_asprintf (&ret, + "%s %s", + tmp, + kc->name); + GNUNET_free (tmp); + } + } + *required = ret; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +void +TALER_KYCLOGIC_kyc_get_details ( + const char *logic_name, + TALER_KYCLOGIC_DetailsCallback cb, + void *cb_cls) +{ + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + if (0 != + strcmp (kp->logic->name, + logic_name)) + continue; + if (GNUNET_OK != + cb (cb_cls, + kp->pd, + kp->logic->cls)) + return; + } +} + + +enum GNUNET_DB_QueryStatus +TALER_KYCLOGIC_check_satisfied (char **requirements, + const struct TALER_PaytoHashP *h_payto, + json_t **kyc_details, + TALER_KYCLOGIC_KycSatisfiedIterator ki, + void *ki_cls, + bool *satisfied) +{ + struct TALER_KYCLOGIC_KycCheck *needed[num_kyc_checks]; + unsigned int needed_cnt = 0; + + if (NULL == requirements) + { + *satisfied = true; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + { + char *req = *requirements; + + for (const char *tok = strtok (req, " "); + NULL != tok; + tok = strtok (NULL, " ")) + needed[needed_cnt++] = add_check (tok); + GNUNET_free (req); + *requirements = NULL; + } + + { + struct RemoveContext rc = { + .needed = needed, + .needed_cnt = &needed_cnt, + }; + enum GNUNET_DB_QueryStatus qs; + + rc.kyc_details = json_object (); + GNUNET_assert (NULL != rc.kyc_details); + + /* Check what provider checks are already satisfied for h_payto (with + database), remove those from the 'needed' array. */ + qs = ki (ki_cls, + h_payto, + &remove_satisfied, + &rc); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + *satisfied = false; + return qs; + } + if (0 != needed_cnt) + { + json_decref (rc.kyc_details); + *kyc_details = NULL; + } + else + { + *kyc_details = rc.kyc_details; + } + } + *satisfied = (0 == needed_cnt); + + { + char *res = NULL; + + for (unsigned int i = 0; i<needed_cnt; i++) + { + const struct TALER_KYCLOGIC_KycCheck *need = needed[i]; + + if (NULL == res) + { + res = GNUNET_strdup (need->name); + } + else + { + char *tmp; + + GNUNET_asprintf (&tmp, + "%s %s", + res, + need->name); + GNUNET_free (res); + res = tmp; + } + } + *requirements = res; + } + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_requirements_to_logic (const char *requirements, + enum TALER_KYCLOGIC_KycUserType ut, + struct TALER_KYCLOGIC_Plugin **plugin, + struct TALER_KYCLOGIC_ProviderDetails **pd, + const char **configuration_section) +{ + struct TALER_KYCLOGIC_KycCheck *needed[num_kyc_checks]; + unsigned int needed_cnt = 0; + unsigned long long min_cost = ULLONG_MAX; + unsigned int max_checks = 0; + const struct TALER_KYCLOGIC_KycProvider *kp_best = NULL; + + if (NULL == requirements) + return GNUNET_NO; + { + char *req = GNUNET_strdup (requirements); + + for (const char *tok = strtok (req, " "); + NULL != tok; + tok = strtok (NULL, " ")) + needed[needed_cnt++] = add_check (tok); + GNUNET_free (req); + } + + /* Count maximum number of remaining checks covered by any + provider */ + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + const struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + unsigned int matched = 0; + + if (kp->user_type != ut) + continue; + for (unsigned int j = 0; j<kp->num_checks; j++) + { + const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; + + for (unsigned int k = 0; k<needed_cnt; k++) + if (kc == needed[k]) + { + matched++; + break; + } + } + max_checks = GNUNET_MAX (max_checks, + matched); + } + if (0 == max_checks) + return GNUNET_SYSERR; + + /* Find min-cost provider covering max_checks. */ + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + const struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + unsigned int matched = 0; + + if (kp->user_type != ut) + continue; + for (unsigned int j = 0; j<kp->num_checks; j++) + { + const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; + + for (unsigned int k = 0; k<needed_cnt; k++) + if (kc == needed[k]) + { + matched++; + break; + } + } + if ( (max_checks == matched) && + (kp->cost < min_cost) ) + { + min_cost = kp->cost; + kp_best = kp; + } + } + GNUNET_assert (NULL != kp_best); + *plugin = kp_best->logic; + *pd = kp_best->pd; + *configuration_section = kp_best->provider_section_name; + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_lookup_logic (const char *name, + struct TALER_KYCLOGIC_Plugin **plugin, + struct TALER_KYCLOGIC_ProviderDetails **pd, + const char **provider_section) +{ + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + if (0 != + strcasecmp (name, + kp->provider_section_name)) + continue; + *plugin = kp->logic; + *pd = kp->pd; + *provider_section = kp->provider_section_name; + return GNUNET_OK; + } + for (unsigned int i = 0; i<num_kyc_logics; i++) + { + struct TALER_KYCLOGIC_Plugin *logic = kyc_logics[i]; + + if (0 != + strcasecmp (logic->name, + name)) + continue; + *plugin = logic; + *pd = NULL; + *provider_section = NULL; + return GNUNET_OK; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Provider `%s' unknown\n", + name); + return GNUNET_SYSERR; +} + + +void +TALER_KYCLOGIC_kyc_iterate_thresholds ( + enum TALER_KYCLOGIC_KycTriggerEvent event, + TALER_KYCLOGIC_KycThresholdIterator it, + void *it_cls) +{ + for (unsigned int i = 0; i<num_kyc_triggers; i++) + { + const struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; + + if (event != kt->trigger) + continue; + it (it_cls, + &kt->threshold); + } +} + + +void +TALER_KYCLOGIC_lookup_checks (const char *section_name, + unsigned int *num_checks, + char ***provided_checks) +{ + *num_checks = 0; + *provided_checks = NULL; + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + if (0 != + strcasecmp (section_name, + kp->provider_section_name)) + continue; + *num_checks = kp->num_checks; + if (0 != kp->num_checks) + { + char **pc = GNUNET_new_array (kp->num_checks, + char *); + for (unsigned int i = 0; i<kp->num_checks; i++) + pc[i] = GNUNET_strdup (kp->provided_checks[i]->name); + *provided_checks = pc; + } + return; + } +} + + +/* end of taler-exchange-httpd_kyc.c */ diff --git a/src/kyclogic/plugin_kyclogic_kycaid.c b/src/kyclogic/plugin_kyclogic_kycaid.c new file mode 100644 index 000000000..243ff7c34 --- /dev/null +++ b/src/kyclogic/plugin_kyclogic_kycaid.c @@ -0,0 +1,1480 @@ +/* + This file is part of GNU Taler + Copyright (C) 2022--2024 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.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file plugin_kyclogic_kycaid.c + * @brief kycaid for an authentication flow logic + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_attributes.h" +#include "taler_kyclogic_lib.h" +#include "taler_kyclogic_plugin.h" +#include "taler_mhd_lib.h" +#include "taler_curl_lib.h" +#include "taler_json_lib.h" +#include "taler_templating_lib.h" +#include <regex.h> +#include "taler_util.h" + + +/** + * Saves the state of a plugin. + */ +struct PluginState +{ + + /** + * Our base URL. + */ + char *exchange_base_url; + + /** + * Our global configuration. + */ + const struct GNUNET_CONFIGURATION_Handle *cfg; + + /** + * Context for CURL operations (useful to the event loop) + */ + struct GNUNET_CURL_Context *curl_ctx; + + /** + * Context for integrating @e curl_ctx with the + * GNUnet event loop. + */ + struct GNUNET_CURL_RescheduleContext *curl_rc; + +}; + + +/** + * Keeps the plugin-specific state for + * a given configuration section. + */ +struct TALER_KYCLOGIC_ProviderDetails +{ + + /** + * Overall plugin state. + */ + struct PluginState *ps; + + /** + * Configuration section that configured us. + */ + char *section; + + /** + * Authorization token to use when talking + * to the service. + */ + char *auth_token; + + /** + * Form ID for the KYC check to perform. + */ + char *form_id; + + /** + * Helper binary to convert attributes returned by + * KYCAID into our internal format. + */ + char *conversion_helper; + + /** + * Validity time for a successful KYC process. + */ + struct GNUNET_TIME_Relative validity; + + /** + * Curl-ready authentication header to use. + */ + struct curl_slist *slist; + +}; + + +/** + * Handle for an initiation operation. + */ +struct TALER_KYCLOGIC_InitiateHandle +{ + + /** + * Hash of the payto:// URI we are initiating + * the KYC for. + */ + struct TALER_PaytoHashP h_payto; + + /** + * UUID being checked. + */ + uint64_t legitimization_uuid; + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Continuation to call. + */ + TALER_KYCLOGIC_InitiateCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * URL of the cURL request. + */ + char *url; + +}; + + +/** + * Handle for an KYC proof operation. + */ +struct TALER_KYCLOGIC_ProofHandle +{ + + /** + * Overall plugin state. + */ + struct PluginState *ps; + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Continuation to call. + */ + TALER_KYCLOGIC_ProofCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Task for asynchronous execution. + */ + struct GNUNET_SCHEDULER_Task *task; +}; + + +/** + * Handle for an KYC Web hook operation. + */ +struct TALER_KYCLOGIC_WebhookHandle +{ + + /** + * Continuation to call when done. + */ + TALER_KYCLOGIC_WebhookCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Task for asynchronous execution. + */ + struct GNUNET_SCHEDULER_Task *task; + + /** + * Overall plugin state. + */ + struct PluginState *ps; + + /** + * Handle to helper process to extract attributes + * we care about. + */ + struct TALER_JSON_ExternalConversion *econ; + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * JSON response we got back, or NULL for none. + */ + json_t *json_response; + + /** + * Verification ID from the service. + */ + char *verification_id; + + /** + * Applicant ID from the service. + */ + char *applicant_id; + + /** + * URL of the cURL request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Response to return asynchronously. + */ + struct MHD_Response *resp; + + /** + * Our account ID. + */ + struct TALER_PaytoHashP h_payto; + + /** + * Row in legitimizations for the given + * @e verification_id. + */ + uint64_t process_row; + + /** + * HTTP response code we got from KYCAID. + */ + unsigned int kycaid_response_code; + + /** + * HTTP response code to return asynchronously. + */ + unsigned int response_code; +}; + + +/** + * Release configuration resources previously loaded + * + * @param[in] pd configuration to release + */ +static void +kycaid_unload_configuration (struct TALER_KYCLOGIC_ProviderDetails *pd) +{ + curl_slist_free_all (pd->slist); + GNUNET_free (pd->conversion_helper); + GNUNET_free (pd->auth_token); + GNUNET_free (pd->form_id); + GNUNET_free (pd->section); + GNUNET_free (pd); +} + + +/** + * Load the configuration of the KYC provider. + * + * @param cls closure + * @param provider_section_name configuration section to parse + * @return NULL if configuration is invalid + */ +static struct TALER_KYCLOGIC_ProviderDetails * +kycaid_load_configuration (void *cls, + const char *provider_section_name) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_ProviderDetails *pd; + + pd = GNUNET_new (struct TALER_KYCLOGIC_ProviderDetails); + pd->ps = ps; + pd->section = GNUNET_strdup (provider_section_name); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (ps->cfg, + provider_section_name, + "KYC_KYCAID_VALIDITY", + &pd->validity)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_KYCAID_VALIDITY"); + kycaid_unload_configuration (pd); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_KYCAID_AUTH_TOKEN", + &pd->auth_token)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_KYCAID_AUTH_TOKEN"); + kycaid_unload_configuration (pd); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_KYCAID_FORM_ID", + &pd->form_id)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_KYCAID_FORM_ID"); + kycaid_unload_configuration (pd); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_KYCAID_CONVERTER_HELPER", + &pd->conversion_helper)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_KYCAID_CONVERTER_HELPER"); + kycaid_unload_configuration (pd); + return NULL; + } + { + char *auth; + + GNUNET_asprintf (&auth, + "%s: Token %s", + MHD_HTTP_HEADER_AUTHORIZATION, + pd->auth_token); + pd->slist = curl_slist_append (NULL, + auth); + GNUNET_free (auth); + } + return pd; +} + + +/** + * Cancel KYC check initiation. + * + * @param[in] ih handle of operation to cancel + */ +static void +kycaid_initiate_cancel (struct TALER_KYCLOGIC_InitiateHandle *ih) +{ + if (NULL != ih->job) + { + GNUNET_CURL_job_cancel (ih->job); + ih->job = NULL; + } + GNUNET_free (ih->url); + TALER_curl_easy_post_finished (&ih->ctx); + GNUNET_free (ih); +} + + +/** + * Function called when we're done processing the + * HTTP "/forms/{form_id}/urls" request. + * + * @param cls the `struct TALER_KYCLOGIC_InitiateHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_initiate_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_KYCLOGIC_InitiateHandle *ih = cls; + const json_t *j = response; + + ih->job = NULL; + switch (response_code) + { + case MHD_HTTP_OK: + { + const char *verification_id; + const char *form_url; + const char *form_id; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("verification_id", + &verification_id), + GNUNET_JSON_spec_string ("form_url", + &form_url), + GNUNET_JSON_spec_string ("form_id", + &form_id), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY, + NULL, + NULL, + NULL, + json_string_value (json_object_get (j, + "type"))); + break; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Started new verification `%s' using form %s\n", + verification_id, + form_id); + ih->cb (ih->cb_cls, + TALER_EC_NONE, + form_url, + NULL, /* no provider_user_id */ + verification_id, + NULL /* no error */); + GNUNET_JSON_parse_free (spec); + } + break; + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_CONFLICT: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYCAID failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_BUG, + NULL, + NULL, + NULL, + json_string_value (json_object_get (j, + "type"))); + break; + case MHD_HTTP_UNAUTHORIZED: + case MHD_HTTP_PAYMENT_REQUIRED: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refused access with HTTP status code %u\n", + (unsigned int) response_code); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED, + NULL, + NULL, + NULL, + json_string_value (json_object_get (j, + "type"))); + break; + case MHD_HTTP_REQUEST_TIMEOUT: + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT, + NULL, + NULL, + NULL, + json_string_value (json_object_get (j, + "type"))); + break; + case MHD_HTTP_UNPROCESSABLE_ENTITY: /* validation */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYCAID failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY, + NULL, + NULL, + NULL, + json_string_value (json_object_get (j, + "type"))); + break; + case MHD_HTTP_TOO_MANY_REQUESTS: + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED, + NULL, + NULL, + NULL, + json_string_value (json_object_get (j, + "type"))); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY, + NULL, + NULL, + NULL, + json_string_value (json_object_get (j, + "type"))); + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected KYCAID response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY, + NULL, + NULL, + NULL, + json_string_value (json_object_get (j, + "type"))); + break; + } + kycaid_initiate_cancel (ih); +} + + +/** + * Initiate KYC check. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param account_id which account to trigger process for + * @param legitimization_uuid unique ID for the legitimization process + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_InitiateHandle * +kycaid_initiate (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + const struct TALER_PaytoHashP *account_id, + uint64_t legitimization_uuid, + TALER_KYCLOGIC_InitiateCallback cb, + void *cb_cls) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_InitiateHandle *ih; + json_t *body; + CURL *eh; + + eh = curl_easy_init (); + if (NULL == eh) + { + GNUNET_break (0); + return NULL; + } + ih = GNUNET_new (struct TALER_KYCLOGIC_InitiateHandle); + ih->legitimization_uuid = legitimization_uuid; + ih->cb = cb; + ih->cb_cls = cb_cls; + ih->h_payto = *account_id; + ih->pd = pd; + GNUNET_asprintf (&ih->url, + "https://api.kycaid.com/forms/%s/urls", + pd->form_id); + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data64_auto ("external_applicant_id", + account_id) + ); + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_VERBOSE, + 0)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_MAXREDIRS, + 1L)); + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + ih->url)); + if (GNUNET_OK != + TALER_curl_easy_post (&ih->ctx, + eh, + body)) + { + GNUNET_break (0); + GNUNET_free (ih->url); + GNUNET_free (ih); + curl_easy_cleanup (eh); + json_decref (body); + return NULL; + } + json_decref (body); + ih->job = GNUNET_CURL_job_add2 (ps->curl_ctx, + eh, + ih->ctx.headers, + &handle_initiate_finished, + ih); + GNUNET_CURL_extend_headers (ih->job, + pd->slist); + return ih; +} + + +/** + * Cancel KYC proof. + * + * @param[in] ph handle of operation to cancel + */ +static void +kycaid_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph) +{ + if (NULL != ph->task) + { + GNUNET_SCHEDULER_cancel (ph->task); + ph->task = NULL; + } + GNUNET_free (ph); +} + + +/** + * Call @a ph callback with HTTP error response. + * + * @param cls proof handle to generate reply for + */ +static void +proof_reply (void *cls) +{ + struct TALER_KYCLOGIC_ProofHandle *ph = cls; + struct MHD_Response *resp; + enum GNUNET_GenericReturnValue ret; + json_t *body; + unsigned int http_status; + + http_status = MHD_HTTP_BAD_REQUEST; + body = GNUNET_JSON_PACK ( + TALER_JSON_pack_ec (TALER_EC_GENERIC_ENDPOINT_UNKNOWN)); + GNUNET_assert (NULL != body); + ret = TALER_TEMPLATING_build (ph->connection, + &http_status, + "kycaid-invalid-request", + NULL, + NULL, + body, + &resp); + json_decref (body); + GNUNET_break (GNUNET_SYSERR != ret); + ph->cb (ph->cb_cls, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + NULL, /* user id */ + NULL, /* provider legi ID */ + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, /* attributes */ + http_status, + resp); +} + + +/** + * Check KYC status and return status to human. Not + * used by KYC AID! + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param connection MHD connection object (for HTTP headers) + * @param account_id which account to trigger process for + * @param process_row row in the legitimization processes table the legitimization is for + * @param provider_user_id user ID (or NULL) the proof is for + * @param provider_legitimization_id legitimization ID the proof is for + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_ProofHandle * +kycaid_proof (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + struct MHD_Connection *connection, + const struct TALER_PaytoHashP *account_id, + uint64_t process_row, + const char *provider_user_id, + const char *provider_legitimization_id, + TALER_KYCLOGIC_ProofCallback cb, + void *cb_cls) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_ProofHandle *ph; + + ph = GNUNET_new (struct TALER_KYCLOGIC_ProofHandle); + ph->ps = ps; + ph->pd = pd; + ph->cb = cb; + ph->cb_cls = cb_cls; + ph->connection = connection; + ph->task = GNUNET_SCHEDULER_add_now (&proof_reply, + ph); + return ph; +} + + +/** + * Cancel KYC webhook execution. + * + * @param[in] wh handle of operation to cancel + */ +static void +kycaid_webhook_cancel (struct TALER_KYCLOGIC_WebhookHandle *wh) +{ + if (NULL != wh->task) + { + GNUNET_SCHEDULER_cancel (wh->task); + wh->task = NULL; + } + if (NULL != wh->econ) + { + TALER_JSON_external_conversion_stop (wh->econ); + wh->econ = NULL; + } + if (NULL != wh->job) + { + GNUNET_CURL_job_cancel (wh->job); + wh->job = NULL; + } + if (NULL != wh->json_response) + { + json_decref (wh->json_response); + wh->json_response = NULL; + } + GNUNET_free (wh->verification_id); + GNUNET_free (wh->applicant_id); + GNUNET_free (wh->url); + GNUNET_free (wh); +} + + +/** + * Extract KYC failure reasons and log those + * + * @param verifications JSON object with failure details + */ +static void +log_failure (const json_t *verifications) +{ + const json_t *member; + const char *name; + + json_object_foreach ((json_t *) verifications, name, member) + { + bool iverified; + const char *comment; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_bool ("verified", + &iverified), + GNUNET_JSON_spec_string ("comment", + &comment), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (member, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + json_dumpf (member, + stderr, + JSON_INDENT (2)); + continue; + } + if (iverified) + continue; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC verification of attribute `%s' failed: %s\n", + name, + comment); + } +} + + +/** + * Type of a callback that receives a JSON @a result. + * + * @param cls closure our `struct TALER_KYCLOGIC_WebhookHandle *` + * @param status_type how did the process die + * @param code termination status code from the process + * @param result converted attribute data, NULL on failure + */ +static void +webhook_conversion_cb (void *cls, + enum GNUNET_OS_ProcessStatusType status_type, + unsigned long code, + const json_t *result) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + struct GNUNET_TIME_Absolute expiration; + struct MHD_Response *resp; + + wh->econ = NULL; + if ( (0 == code) && + (NULL == result) ) + { + /* No result, but *our helper* was OK => bad input */ + GNUNET_break_op (0); + json_dumpf (wh->json_response, + stderr, + JSON_INDENT (2)); + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + wh->kycaid_response_code), + GNUNET_JSON_pack_object_incref ("kycaid_body", + (json_t *) wh->json_response)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_BAD_GATEWAY, + resp); + kycaid_webhook_cancel (wh); + return; + } + if (NULL == result) + { + /* Failure in our helper */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Helper exited with status code %d\n", + (int) code); + json_dumpf (wh->json_response, + stderr, + JSON_INDENT (2)); + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + wh->kycaid_response_code), + GNUNET_JSON_pack_object_incref ("kycaid_body", + (json_t *) wh->json_response)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_BAD_GATEWAY, + resp); + kycaid_webhook_cancel (wh); + return; + } + expiration = GNUNET_TIME_relative_to_absolute (wh->pd->validity); + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_SUCCESS, + expiration, + result, + MHD_HTTP_NO_CONTENT, + resp); + kycaid_webhook_cancel (wh); +} + + +/** + * Function called when we're done processing the + * HTTP "/applicants/{verification_id}" request. + * + * @param cls the `struct TALER_KYCLOGIC_WebhookHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_webhook_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + const json_t *j = response; + struct MHD_Response *resp; + + wh->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Webhook returned with HTTP status %u\n", + (unsigned int) response_code); + wh->kycaid_response_code = response_code; + wh->json_response = json_incref ((json_t *) j); + switch (response_code) + { + case MHD_HTTP_OK: + { + const char *profile_status; + + profile_status = json_string_value ( + json_object_get ( + j, + "profile_status")); + if (0 != strcasecmp ("valid", + profile_status)) + { + enum TALER_KYCLOGIC_KycStatus ks; + + ks = (0 == strcasecmp ("pending", + profile_status)) + ? TALER_KYCLOGIC_STATUS_PENDING + : TALER_KYCLOGIC_STATUS_USER_ABORTED; + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + ks, + GNUNET_TIME_UNIT_ZERO_ABS, + NULL, + MHD_HTTP_NO_CONTENT, + resp); + break; + } + wh->econ + = TALER_JSON_external_conversion_start ( + j, + &webhook_conversion_cb, + wh, + wh->pd->conversion_helper, + wh->pd->conversion_helper, + "-a", + wh->pd->auth_token, + NULL); + if (NULL == wh->econ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start KYCAID conversion helper `%s'\n", + wh->pd->conversion_helper); + resp = TALER_MHD_make_error ( + TALER_EC_EXCHANGE_GENERIC_KYC_CONVERTER_FAILED, + NULL); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_INTERNAL_ERROR, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_INTERNAL_SERVER_ERROR, + resp); + break; + } + return; + } + break; + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_CONFLICT: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYCAID failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + response_code)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_INTERNAL_SERVER_ERROR, + resp); + break; + case MHD_HTTP_UNAUTHORIZED: + case MHD_HTTP_PAYMENT_REQUIRED: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refused access with HTTP status code %u\n", + (unsigned int) response_code); + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + response_code), + GNUNET_JSON_pack_object_incref ("kycaid_body", + (json_t *) j)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_NETWORK_AUTHENTICATION_REQUIRED, + resp); + break; + case MHD_HTTP_REQUEST_TIMEOUT: + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + response_code), + GNUNET_JSON_pack_object_incref ("kycaid_body", + (json_t *) j)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_GATEWAY_TIMEOUT, + resp); + break; + case MHD_HTTP_UNPROCESSABLE_ENTITY: /* validation */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYCAID failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + response_code), + GNUNET_JSON_pack_object_incref ("kycaid_body", + (json_t *) j)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_BAD_GATEWAY, + resp); + break; + case MHD_HTTP_TOO_MANY_REQUESTS: + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + response_code), + GNUNET_JSON_pack_object_incref ("kycaid_body", + (json_t *) j)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_SERVICE_UNAVAILABLE, + resp); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + response_code), + GNUNET_JSON_pack_object_incref ("kycaid_body", + (json_t *) j)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_BAD_GATEWAY, + resp); + break; + default: + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + response_code), + GNUNET_JSON_pack_object_incref ("kycaid_body", + (json_t *) j)); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected KYCAID response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_BAD_GATEWAY, + resp); + break; + } + kycaid_webhook_cancel (wh); +} + + +/** + * Asynchronously return a reply for the webhook. + * + * @param cls a `struct TALER_KYCLOGIC_WebhookHandle *` + */ +static void +async_webhook_reply (void *cls) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + + wh->task = NULL; + wh->cb (wh->cb_cls, + wh->process_row, + (0 == wh->process_row) + ? NULL + : &wh->h_payto, + wh->pd->section, + wh->applicant_id, /* provider user ID */ + wh->verification_id, /* provider legi ID */ + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + wh->response_code, + wh->resp); + kycaid_webhook_cancel (wh); +} + + +/** + * Check KYC status and return result for Webhook. We do NOT implement the + * authentication check proposed by the KYCAID documentation, as it would + * allow an attacker who learns the access token to easily bypass the KYC + * checks. Instead, we insist on explicitly requesting the KYC status from the + * provider (at least on success). + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param plc callback to lookup accounts with + * @param plc_cls closure for @a plc + * @param http_method HTTP method used for the webhook + * @param url_path rest of the URL after `/kyc-webhook/` + * @param connection MHD connection object (for HTTP headers) + * @param body HTTP request body + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_WebhookHandle * +kycaid_webhook (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + TALER_KYCLOGIC_ProviderLookupCallback plc, + void *plc_cls, + const char *http_method, + const char *const url_path[], + struct MHD_Connection *connection, + const json_t *body, + TALER_KYCLOGIC_WebhookCallback cb, + void *cb_cls) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_WebhookHandle *wh; + CURL *eh; + const char *request_id; + const char *type; + const char *verification_id; /* = provider_legitimization_id */ + const char *applicant_id; + const char *form_id; + const char *status = NULL; + bool verified = false; + bool no_verified = true; + const json_t *verifications = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("request_id", + &request_id), + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_string ("verification_id", + &verification_id), + GNUNET_JSON_spec_string ("applicant_id", + &applicant_id), + GNUNET_JSON_spec_string ("form_id", + &form_id), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("status", + &status), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("verified", + &verified), + &no_verified), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("verifications", + &verifications), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_DB_QueryStatus qs; + + wh = GNUNET_new (struct TALER_KYCLOGIC_WebhookHandle); + wh->cb = cb; + wh->cb_cls = cb_cls; + wh->ps = ps; + wh->pd = pd; + wh->connection = connection; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYCAID webhook of `%s' triggered with %s\n", + pd->section, + http_method); +#if 1 + if (NULL != body) + json_dumpf (body, + stderr, + JSON_INDENT (2)); +#endif + if (NULL == pd) + { + GNUNET_break_op (0); + json_dumpf (body, + stderr, + JSON_INDENT (2)); + wh->resp = TALER_MHD_make_error ( + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, + "kycaid"); + wh->response_code = MHD_HTTP_NOT_FOUND; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (body, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + json_dumpf (body, + stderr, + JSON_INDENT (2)); + wh->resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("webhook_body", + (json_t *) body)); + wh->response_code = MHD_HTTP_BAD_REQUEST; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + qs = plc (plc_cls, + pd->section, + verification_id, + &wh->h_payto, + &wh->process_row); + if (qs < 0) + { + wh->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, + "provider-legitimization-lookup"); + wh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received webhook for unknown verification ID `%s' and section `%s'\n", + verification_id, + pd->section); + wh->resp = TALER_MHD_make_error ( + TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, + verification_id); + wh->response_code = MHD_HTTP_NOT_FOUND; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + wh->verification_id = GNUNET_strdup (verification_id); + wh->applicant_id = GNUNET_strdup (applicant_id); + if ( (0 != strcasecmp (type, + "VERIFICATION_COMPLETED")) || + (no_verified) || + (! verified) ) + { + /* We don't need to re-confirm the failure by + asking the API again. */ + log_failure (verifications); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Webhook called with non-completion status: %s\n", + type); + wh->response_code = MHD_HTTP_NO_CONTENT; + wh->resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + + eh = curl_easy_init (); + if (NULL == eh) + { + GNUNET_break (0); + wh->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_ALLOCATION_FAILURE, + NULL); + wh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + + GNUNET_asprintf (&wh->url, + "https://api.kycaid.com/applicants/%s", + applicant_id); + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_VERBOSE, + 0)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_MAXREDIRS, + 1L)); + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + wh->url)); + wh->job = GNUNET_CURL_job_add2 (ps->curl_ctx, + eh, + pd->slist, + &handle_webhook_finished, + wh); + return wh; +} + + +/** + * Initialize kycaid logic plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct TALER_KYCLOGIC_Plugin` + */ +void * +libtaler_plugin_kyclogic_kycaid_init (void *cls) +{ + const struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct TALER_KYCLOGIC_Plugin *plugin; + struct PluginState *ps; + + ps = GNUNET_new (struct PluginState); + ps->cfg = cfg; + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "exchange", + "BASE_URL", + &ps->exchange_base_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "BASE_URL"); + GNUNET_free (ps); + return NULL; + } + + ps->curl_ctx + = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &ps->curl_rc); + if (NULL == ps->curl_ctx) + { + GNUNET_break (0); + GNUNET_free (ps->exchange_base_url); + GNUNET_free (ps); + return NULL; + } + ps->curl_rc = GNUNET_CURL_gnunet_rc_create (ps->curl_ctx); + + plugin = GNUNET_new (struct TALER_KYCLOGIC_Plugin); + plugin->cls = ps; + plugin->load_configuration + = &kycaid_load_configuration; + plugin->unload_configuration + = &kycaid_unload_configuration; + plugin->initiate + = &kycaid_initiate; + plugin->initiate_cancel + = &kycaid_initiate_cancel; + plugin->proof + = &kycaid_proof; + plugin->proof_cancel + = &kycaid_proof_cancel; + plugin->webhook + = &kycaid_webhook; + plugin->webhook_cancel + = &kycaid_webhook_cancel; + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct TALER_KYCLOGIC_Plugin` + * @return NULL (always) + */ +void * +libtaler_plugin_kyclogic_kycaid_done (void *cls) +{ + struct TALER_KYCLOGIC_Plugin *plugin = cls; + struct PluginState *ps = plugin->cls; + + if (NULL != ps->curl_ctx) + { + GNUNET_CURL_fini (ps->curl_ctx); + ps->curl_ctx = NULL; + } + if (NULL != ps->curl_rc) + { + GNUNET_CURL_gnunet_rc_destroy (ps->curl_rc); + ps->curl_rc = NULL; + } + GNUNET_free (ps->exchange_base_url); + GNUNET_free (ps); + GNUNET_free (plugin); + return NULL; +} diff --git a/src/kyclogic/plugin_kyclogic_oauth2.c b/src/kyclogic/plugin_kyclogic_oauth2.c new file mode 100644 index 000000000..3a1f50bcf --- /dev/null +++ b/src/kyclogic/plugin_kyclogic_oauth2.c @@ -0,0 +1,1780 @@ +/* + This file is part of GNU Taler + Copyright (C) 2022-2024 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.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file plugin_kyclogic_oauth2.c + * @brief oauth2.0 based authentication flow logic + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_kyclogic_plugin.h" +#include "taler_mhd_lib.h" +#include "taler_templating_lib.h" +#include "taler_json_lib.h" +#include <regex.h> +#include "taler_util.h" + + +/** + * Saves the state of a plugin. + */ +struct PluginState +{ + + /** + * Our global configuration. + */ + const struct GNUNET_CONFIGURATION_Handle *cfg; + + /** + * Our base URL. + */ + char *exchange_base_url; + + /** + * Context for CURL operations (useful to the event loop) + */ + struct GNUNET_CURL_Context *curl_ctx; + + /** + * Context for integrating @e curl_ctx with the + * GNUnet event loop. + */ + struct GNUNET_CURL_RescheduleContext *curl_rc; + +}; + + +/** + * Keeps the plugin-specific state for + * a given configuration section. + */ +struct TALER_KYCLOGIC_ProviderDetails +{ + + /** + * Overall plugin state. + */ + struct PluginState *ps; + + /** + * Configuration section that configured us. + */ + char *section; + + /** + * URL of the Challenger ``/setup`` endpoint for + * approving address validations. NULL if not used. + */ + char *setup_url; + + /** + * URL of the OAuth2.0 endpoint for KYC checks. + */ + char *authorize_url; + + /** + * URL of the OAuth2.0 endpoint for KYC checks. + * (token/auth) + */ + char *token_url; + + /** + * URL of the user info access endpoint. + */ + char *info_url; + + /** + * Our client ID for OAuth2.0. + */ + char *client_id; + + /** + * Our client secret for OAuth2.0. + */ + char *client_secret; + + /** + * Where to redirect clients after the + * Web-based KYC process is done? + */ + char *post_kyc_redirect_url; + + /** + * Name of the program we use to convert outputs + * from Persona into our JSON inputs. + */ + char *conversion_binary; + + /** + * Validity time for a successful KYC process. + */ + struct GNUNET_TIME_Relative validity; + + /** + * Set to true if we are operating in DEBUG + * mode and may return private details in HTML + * responses to make diagnostics easier. + */ + bool debug_mode; +}; + + +/** + * Handle for an initiation operation. + */ +struct TALER_KYCLOGIC_InitiateHandle +{ + + /** + * Hash of the payto:// URI we are initiating + * the KYC for. + */ + struct TALER_PaytoHashP h_payto; + + /** + * UUID being checked. + */ + uint64_t legitimization_uuid; + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * The task for asynchronous response generation. + */ + struct GNUNET_SCHEDULER_Task *task; + + /** + * Handle for the OAuth 2.0 setup request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Continuation to call. + */ + TALER_KYCLOGIC_InitiateCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + +}; + + +/** + * Handle for an KYC proof operation. + */ +struct TALER_KYCLOGIC_ProofHandle +{ + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * HTTP connection we are processing. + */ + struct MHD_Connection *connection; + + /** + * Handle to an external process that converts the + * Persona response to our internal format. + */ + struct TALER_JSON_ExternalConversion *ec; + + /** + * Hash of the payto URI that this is about. + */ + struct TALER_PaytoHashP h_payto; + + /** + * Continuation to call. + */ + TALER_KYCLOGIC_ProofCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Curl request we are running to the OAuth 2.0 service. + */ + CURL *eh; + + /** + * Body for the @e eh POST request. + */ + char *post_body; + + /** + * KYC attributes returned about the user by the OAuth 2.0 server. + */ + json_t *attributes; + + /** + * Response to return. + */ + struct MHD_Response *response; + + /** + * The task for asynchronous response generation. + */ + struct GNUNET_SCHEDULER_Task *task; + + /** + * Handle for the OAuth 2.0 CURL request. + */ + struct GNUNET_CURL_Job *job; + + /** + * User ID to return, the 'id' from OAuth. + */ + char *provider_user_id; + + /** + * Legitimization ID to return, the 64-bit row ID + * as a string. + */ + char provider_legitimization_id[32]; + + /** + * KYC status to return. + */ + enum TALER_KYCLOGIC_KycStatus status; + + /** + * HTTP status to return. + */ + unsigned int http_status; + + +}; + + +/** + * Handle for an KYC Web hook operation. + */ +struct TALER_KYCLOGIC_WebhookHandle +{ + + /** + * Continuation to call when done. + */ + TALER_KYCLOGIC_WebhookCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Task for asynchronous execution. + */ + struct GNUNET_SCHEDULER_Task *task; + + /** + * Overall plugin state. + */ + struct PluginState *ps; +}; + + +/** + * Release configuration resources previously loaded + * + * @param[in] pd configuration to release + */ +static void +oauth2_unload_configuration (struct TALER_KYCLOGIC_ProviderDetails *pd) +{ + GNUNET_free (pd->section); + GNUNET_free (pd->token_url); + GNUNET_free (pd->setup_url); + GNUNET_free (pd->authorize_url); + GNUNET_free (pd->info_url); + GNUNET_free (pd->client_id); + GNUNET_free (pd->client_secret); + GNUNET_free (pd->post_kyc_redirect_url); + GNUNET_free (pd->conversion_binary); + GNUNET_free (pd); +} + + +/** + * Load the configuration of the KYC provider. + * + * @param cls closure + * @param provider_section_name configuration section to parse + * @return NULL if configuration is invalid + */ +static struct TALER_KYCLOGIC_ProviderDetails * +oauth2_load_configuration (void *cls, + const char *provider_section_name) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_ProviderDetails *pd; + char *s; + + pd = GNUNET_new (struct TALER_KYCLOGIC_ProviderDetails); + pd->ps = ps; + pd->section = GNUNET_strdup (provider_section_name); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (ps->cfg, + provider_section_name, + "KYC_OAUTH2_VALIDITY", + &pd->validity)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_VALIDITY"); + oauth2_unload_configuration (pd); + return NULL; + } + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_OAUTH2_CLIENT_ID", + &s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_CLIENT_ID"); + oauth2_unload_configuration (pd); + return NULL; + } + pd->client_id = s; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_OAUTH2_TOKEN_URL", + &s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_TOKEN_URL"); + oauth2_unload_configuration (pd); + return NULL; + } + if ( (! TALER_url_valid_charset (s)) || + ( (0 != strncasecmp (s, + "http://", + strlen ("http://"))) && + (0 != strncasecmp (s, + "https://", + strlen ("https://"))) ) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_TOKEN_URL", + "not a valid URL"); + GNUNET_free (s); + oauth2_unload_configuration (pd); + return NULL; + } + pd->token_url = s; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_OAUTH2_AUTHORIZE_URL", + &s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_AUTHORIZE_URL"); + oauth2_unload_configuration (pd); + return NULL; + } + if ( (! TALER_url_valid_charset (s)) || + ( (0 != strncasecmp (s, + "http://", + strlen ("http://"))) && + (0 != strncasecmp (s, + "https://", + strlen ("https://"))) ) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_AUTHORIZE_URL", + "not a valid URL"); + oauth2_unload_configuration (pd); + GNUNET_free (s); + return NULL; + } + if (NULL != strchr (s, '#')) + { + const char *extra = strchr (s, '#'); + const char *slash = strrchr (s, '/'); + + if ( (0 != strcmp (extra, + "#setup")) || + (NULL == slash) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_AUTHORIZE_URL", + "not a valid authorze URL (bad fragment)"); + oauth2_unload_configuration (pd); + GNUNET_free (s); + return NULL; + } + pd->authorize_url = GNUNET_strndup (s, + extra - s); + GNUNET_asprintf (&pd->setup_url, + "%.*s/setup/%s", + (int) (slash - s), + s, + pd->client_id); + GNUNET_free (s); + } + else + { + pd->authorize_url = s; + } + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_OAUTH2_INFO_URL", + &s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_INFO_URL"); + oauth2_unload_configuration (pd); + return NULL; + } + if ( (! TALER_url_valid_charset (s)) || + ( (0 != strncasecmp (s, + "http://", + strlen ("http://"))) && + (0 != strncasecmp (s, + "https://", + strlen ("https://"))) ) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_INFO_URL", + "not a valid URL"); + GNUNET_free (s); + oauth2_unload_configuration (pd); + return NULL; + } + pd->info_url = s; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_OAUTH2_CLIENT_SECRET", + &s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_CLIENT_SECRET"); + oauth2_unload_configuration (pd); + return NULL; + } + pd->client_secret = s; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_OAUTH2_POST_URL", + &s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_POST_URL"); + oauth2_unload_configuration (pd); + return NULL; + } + pd->post_kyc_redirect_url = s; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_OAUTH2_CONVERTER_HELPER", + &pd->conversion_binary)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_CONVERTER_HELPER"); + oauth2_unload_configuration (pd); + return NULL; + } + if (GNUNET_OK == + GNUNET_CONFIGURATION_get_value_yesno (ps->cfg, + provider_section_name, + "KYC_OAUTH2_DEBUG_MODE")) + pd->debug_mode = true; + + return pd; +} + + +/** + * Logic to asynchronously return the response for + * how to begin the OAuth2.0 checking process to + * the client. + * + * @param ih process to redirect for + * @param authorize_url authorization URL to use + */ +static void +initiate_with_url (struct TALER_KYCLOGIC_InitiateHandle *ih, + const char *authorize_url) +{ + + const struct TALER_KYCLOGIC_ProviderDetails *pd = ih->pd; + struct PluginState *ps = pd->ps; + char *hps; + char *url; + char legi_s[42]; + + GNUNET_snprintf (legi_s, + sizeof (legi_s), + "%llu", + (unsigned long long) ih->legitimization_uuid); + hps = GNUNET_STRINGS_data_to_string_alloc (&ih->h_payto, + sizeof (ih->h_payto)); + { + char *redirect_uri_encoded; + + { + char *redirect_uri; + + GNUNET_asprintf (&redirect_uri, + "%skyc-proof/%s", + ps->exchange_base_url, + pd->section); + redirect_uri_encoded = TALER_urlencode (redirect_uri); + GNUNET_free (redirect_uri); + } + GNUNET_asprintf (&url, + "%s?response_type=code&client_id=%s&redirect_uri=%s&state=%s", + authorize_url, + pd->client_id, + redirect_uri_encoded, + hps); + GNUNET_free (redirect_uri_encoded); + } + ih->cb (ih->cb_cls, + TALER_EC_NONE, + url, + NULL /* unknown user_id here */, + legi_s, + NULL /* no error */); + GNUNET_free (url); + GNUNET_free (hps); + GNUNET_free (ih); +} + + +/** + * After we are done with the CURL interaction we + * need to update our database state with the information + * retrieved. + * + * @param cls a `struct TALER_KYCLOGIC_InitiateHandle *` + * @param response_code HTTP response code from server, 0 on hard error + * @param response in JSON, NULL if response was not in JSON format + */ +static void +handle_curl_setup_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_KYCLOGIC_InitiateHandle *ih = cls; + const struct TALER_KYCLOGIC_ProviderDetails *pd = ih->pd; + const json_t *j = response; + + ih->job = NULL; + switch (response_code) + { + case 0: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "/setup URL failed to return HTTP response\n"); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, + NULL, + NULL, + NULL, + "/setup request to OAuth 2.0 backend returned no response"); + GNUNET_free (ih); + return; + case MHD_HTTP_OK: + { + const char *nonce; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("nonce", + &nonce), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *emsg; + unsigned int line; + char *url; + + res = GNUNET_JSON_parse (j, + spec, + &emsg, + &line); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, + NULL, + NULL, + NULL, + "Unexpected response from KYC gateway: setup must return a nonce"); + GNUNET_free (ih); + return; + } + GNUNET_asprintf (&url, + "%s/%s", + pd->authorize_url, + nonce); + initiate_with_url (ih, + url); + GNUNET_free (url); + return; + } + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "/setup URL returned HTTP status %u\n", + (unsigned int) response_code); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, + NULL, + NULL, + NULL, + "/setup request to OAuth 2.0 backend returned unexpected HTTP status code"); + GNUNET_free (ih); + return; + } +} + + +/** + * Logic to asynchronously return the response for how to begin the OAuth2.0 + * checking process to the client. May first request a dynamic URL via + * ``/setup`` if configured to use a client-authenticated setup process. + * + * @param cls a `struct TALER_KYCLOGIC_InitiateHandle *` + */ +static void +initiate_task (void *cls) +{ + struct TALER_KYCLOGIC_InitiateHandle *ih = cls; + const struct TALER_KYCLOGIC_ProviderDetails *pd = ih->pd; + struct PluginState *ps = pd->ps; + char *hdr; + struct curl_slist *slist; + CURL *eh; + + ih->task = NULL; + if (NULL == pd->setup_url) + { + initiate_with_url (ih, + pd->authorize_url); + return; + } + eh = curl_easy_init (); + if (NULL == eh) + { + GNUNET_break (0); + ih->cb (ih->cb_cls, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + NULL, + NULL, + NULL, + "curl_easy_init() failed"); + GNUNET_free (ih); + return; + } + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + pd->setup_url)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POST, + 1)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDS, + "")); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_FOLLOWLOCATION, + 1L)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_MAXREDIRS, + 5L)); + GNUNET_asprintf (&hdr, + "%s: Bearer %s", + MHD_HTTP_HEADER_AUTHORIZATION, + pd->client_secret); + slist = curl_slist_append (NULL, + hdr); + ih->job = GNUNET_CURL_job_add2 (ps->curl_ctx, + eh, + slist, + &handle_curl_setup_finished, + ih); + curl_slist_free_all (slist); + GNUNET_free (hdr); +} + + +/** + * Initiate KYC check. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param account_id which account to trigger process for + * @param legitimization_uuid unique ID for the legitimization process + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_InitiateHandle * +oauth2_initiate (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + const struct TALER_PaytoHashP *account_id, + uint64_t legitimization_uuid, + TALER_KYCLOGIC_InitiateCallback cb, + void *cb_cls) +{ + struct TALER_KYCLOGIC_InitiateHandle *ih; + + (void) cls; + ih = GNUNET_new (struct TALER_KYCLOGIC_InitiateHandle); + ih->legitimization_uuid = legitimization_uuid; + ih->cb = cb; + ih->cb_cls = cb_cls; + ih->h_payto = *account_id; + ih->pd = pd; + ih->task = GNUNET_SCHEDULER_add_now (&initiate_task, + ih); + return ih; +} + + +/** + * Cancel KYC check initiation. + * + * @param[in] ih handle of operation to cancel + */ +static void +oauth2_initiate_cancel (struct TALER_KYCLOGIC_InitiateHandle *ih) +{ + if (NULL != ih->task) + { + GNUNET_SCHEDULER_cancel (ih->task); + ih->task = NULL; + } + if (NULL != ih->job) + { + GNUNET_CURL_job_cancel (ih->job); + ih->job = NULL; + } + GNUNET_free (ih); +} + + +/** + * Cancel KYC proof. + * + * @param[in] ph handle of operation to cancel + */ +static void +oauth2_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph) +{ + if (NULL != ph->ec) + { + TALER_JSON_external_conversion_stop (ph->ec); + ph->ec = NULL; + } + if (NULL != ph->task) + { + GNUNET_SCHEDULER_cancel (ph->task); + ph->task = NULL; + } + if (NULL != ph->job) + { + GNUNET_CURL_job_cancel (ph->job); + ph->job = NULL; + } + if (NULL != ph->response) + { + MHD_destroy_response (ph->response); + ph->response = NULL; + } + GNUNET_free (ph->provider_user_id); + if (NULL != ph->attributes) + json_decref (ph->attributes); + GNUNET_free (ph->post_body); + GNUNET_free (ph); +} + + +/** + * Function called to asynchronously return the final + * result to the callback. + * + * @param cls a `struct TALER_KYCLOGIC_ProofHandle` + */ +static void +return_proof_response (void *cls) +{ + struct TALER_KYCLOGIC_ProofHandle *ph = cls; + + ph->task = NULL; + ph->cb (ph->cb_cls, + ph->status, + ph->provider_user_id, + ph->provider_legitimization_id, + GNUNET_TIME_relative_to_absolute (ph->pd->validity), + ph->attributes, + ph->http_status, + ph->response); + ph->response = NULL; /*Ownership passed to 'ph->cb'!*/ + oauth2_proof_cancel (ph); +} + + +/** + * The request for @a ph failed. We may have gotten a useful error + * message in @a j. Generate a failure response. + * + * @param[in,out] ph request that failed + * @param j reply from the server (or NULL) + */ +static void +handle_proof_error (struct TALER_KYCLOGIC_ProofHandle *ph, + const json_t *j) +{ + enum GNUNET_GenericReturnValue res; + + { + const char *msg; + const char *desc; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("error", + &msg), + GNUNET_JSON_spec_string ("error_description", + &desc), + GNUNET_JSON_spec_end () + }; + const char *emsg; + unsigned int line; + + res = GNUNET_JSON_parse (j, + spec, + &emsg, + &line); + } + + if (GNUNET_OK != res) + { + json_t *body; + + GNUNET_break_op (0); + ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; + ph->http_status + = MHD_HTTP_BAD_GATEWAY; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("server_response", + (json_t *) j)), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_assert (NULL != body); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-authorization-failure-malformed", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + return; + } + ph->status = TALER_KYCLOGIC_STATUS_USER_ABORTED; + ph->http_status = MHD_HTTP_FORBIDDEN; + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-authorization-failure", + NULL, + NULL, + j, + &ph->response)); +} + + +/** + * Type of a callback that receives a JSON @a result. + * + * @param cls closure with a `struct TALER_KYCLOGIC_ProofHandle *` + * @param status_type how did the process die + * @param code termination status code from the process + * @param attr result some JSON result, NULL if we failed to get an JSON output + */ +static void +converted_proof_cb (void *cls, + enum GNUNET_OS_ProcessStatusType status_type, + unsigned long code, + const json_t *attr) +{ + struct TALER_KYCLOGIC_ProofHandle *ph = cls; + const struct TALER_KYCLOGIC_ProviderDetails *pd = ph->pd; + + ph->ec = NULL; + if ( (NULL == attr) || + (0 != code) ) + { + json_t *body; + char *msg; + + GNUNET_break_op (0); + ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; + ph->http_status = MHD_HTTP_BAD_GATEWAY; + if (0 != code) + GNUNET_asprintf (&msg, + "Attribute converter exited with status %ld", + code); + else + msg = GNUNET_strdup ( + "Attribute converter response was not in JSON format"); + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("converter", + pd->conversion_binary), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("attributes", + (json_t *) attr)), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_string ("message", + msg), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_free (msg); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-conversion-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); + return; + } + + { + const char *id; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("id", + &id), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *emsg; + unsigned int line; + + res = GNUNET_JSON_parse (attr, + ispec, + &emsg, + &line); + if (GNUNET_OK != res) + { + json_t *body; + + GNUNET_break_op (0); + ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; + ph->http_status = MHD_HTTP_BAD_GATEWAY; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("converter", + pd->conversion_binary), + GNUNET_JSON_pack_string ("message", + "Unexpected response from KYC attribute converter: returned JSON data must contain 'id' field"), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_object_incref ("attributes", + (json_t *) attr), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-conversion-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); + return; + } + ph->provider_user_id = GNUNET_strdup (id); + } + ph->status = TALER_KYCLOGIC_STATUS_SUCCESS; + ph->response = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + GNUNET_assert (NULL != ph->response); + GNUNET_break (MHD_YES == + MHD_add_response_header ( + ph->response, + MHD_HTTP_HEADER_LOCATION, + ph->pd->post_kyc_redirect_url)); + ph->http_status = MHD_HTTP_SEE_OTHER; + ph->attributes = json_incref ((json_t *) attr); + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); +} + + +/** + * The request for @a ph succeeded (presumably). + * Call continuation with the result. + * + * @param[in,out] ph request that succeeded + * @param j reply from the server + */ +static void +parse_proof_success_reply (struct TALER_KYCLOGIC_ProofHandle *ph, + const json_t *j) +{ + const struct TALER_KYCLOGIC_ProviderDetails *pd = ph->pd; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Calling converter `%s' with JSON\n", + pd->conversion_binary); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + ph->ec = TALER_JSON_external_conversion_start ( + j, + &converted_proof_cb, + ph, + pd->conversion_binary, + pd->conversion_binary, + NULL); + if (NULL != ph->ec) + return; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start KYCAID conversion helper `%s'\n", + pd->conversion_binary); + ph->status = TALER_KYCLOGIC_STATUS_INTERNAL_ERROR; + ph->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + { + json_t *body; + + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("converter", + pd->conversion_binary), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_string ("message", + "Failed to launch KYC conversion helper process."), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_GENERIC_KYC_CONVERTER_FAILED)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-conversion-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + } + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); +} + + +/** + * After we are done with the CURL interaction we + * need to update our database state with the information + * retrieved. + * + * @param cls our `struct TALER_KYCLOGIC_ProofHandle` + * @param response_code HTTP response code from server, 0 on hard error + * @param response in JSON, NULL if response was not in JSON format + */ +static void +handle_curl_proof_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_KYCLOGIC_ProofHandle *ph = cls; + const json_t *j = response; + + ph->job = NULL; + switch (response_code) + { + case 0: + { + json_t *body; + + ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; + ph->http_status = MHD_HTTP_BAD_GATEWAY; + + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("message", + "No response from KYC gateway"), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-provider-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + } + break; + case MHD_HTTP_OK: + parse_proof_success_reply (ph, + j); + return; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "OAuth2.0 info URL returned HTTP status %u\n", + (unsigned int) response_code); + handle_proof_error (ph, + j); + break; + } + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); +} + + +/** + * After we are done with the CURL interaction we + * need to fetch the user's account details. + * + * @param cls our `struct KycProofContext` + * @param response_code HTTP response code from server, 0 on hard error + * @param response in JSON, NULL if response was not in JSON format + */ +static void +handle_curl_login_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_KYCLOGIC_ProofHandle *ph = cls; + const json_t *j = response; + + ph->job = NULL; + switch (response_code) + { + case MHD_HTTP_OK: + { + const char *access_token; + const char *token_type; + uint64_t expires_in_s; + const char *refresh_token; + bool no_expires; + bool no_refresh; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("access_token", + &access_token), + GNUNET_JSON_spec_string ("token_type", + &token_type), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("expires_in", + &expires_in_s), + &no_expires), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("refresh_token", + &refresh_token), + &no_refresh), + GNUNET_JSON_spec_end () + }; + CURL *eh; + + { + enum GNUNET_GenericReturnValue res; + const char *emsg; + unsigned int line; + + res = GNUNET_JSON_parse (j, + spec, + &emsg, + &line); + if (GNUNET_OK != res) + { + json_t *body; + + GNUNET_break_op (0); + ph->http_status + = MHD_HTTP_BAD_GATEWAY; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("server_response", + (json_t *) j), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_string ("message", + "Unexpected response from KYC gateway: required fields missing or malformed"), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-provider-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + break; + } + } + if (0 != strcasecmp (token_type, + "bearer")) + { + json_t *body; + + GNUNET_break_op (0); + ph->http_status = MHD_HTTP_BAD_GATEWAY; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("server_response", + (json_t *) j), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_string ("message", + "Unexpected 'token_type' in response from KYC gateway: 'bearer' token required"), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-provider-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + break; + } + + /* We guard against a few characters that could + conceivably be abused to mess with the HTTP header */ + if ( (NULL != strchr (access_token, + '\n')) || + (NULL != strchr (access_token, + '\r')) || + (NULL != strchr (access_token, + ' ')) || + (NULL != strchr (access_token, + ';')) ) + { + json_t *body; + + GNUNET_break_op (0); + ph->http_status = MHD_HTTP_BAD_GATEWAY; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("server_response", + (json_t *) j), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_string ("message", + "Illegal character in access token"), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-provider-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + break; + } + + eh = curl_easy_init (); + GNUNET_assert (NULL != eh); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + ph->pd->info_url)); + { + char *hdr; + struct curl_slist *slist; + + GNUNET_asprintf (&hdr, + "%s: Bearer %s", + MHD_HTTP_HEADER_AUTHORIZATION, + access_token); + slist = curl_slist_append (NULL, + hdr); + ph->job = GNUNET_CURL_job_add2 (ph->pd->ps->curl_ctx, + eh, + slist, + &handle_curl_proof_finished, + ph); + curl_slist_free_all (slist); + GNUNET_free (hdr); + } + return; + } + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "OAuth2.0 login URL returned HTTP status %u\n", + (unsigned int) response_code); + handle_proof_error (ph, + j); + break; + } + return_proof_response (ph); +} + + +/** + * Check KYC status and return status to human. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param connection MHD connection object (for HTTP headers) + * @param account_id which account to trigger process for + * @param process_row row in the legitimization processes table the legitimization is for + * @param provider_user_id user ID (or NULL) the proof is for + * @param provider_legitimization_id legitimization ID the proof is for + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_ProofHandle * +oauth2_proof (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + struct MHD_Connection *connection, + const struct TALER_PaytoHashP *account_id, + uint64_t process_row, + const char *provider_user_id, + const char *provider_legitimization_id, + TALER_KYCLOGIC_ProofCallback cb, + void *cb_cls) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_ProofHandle *ph; + const char *code; + + GNUNET_break (NULL == provider_user_id); + ph = GNUNET_new (struct TALER_KYCLOGIC_ProofHandle); + GNUNET_snprintf (ph->provider_legitimization_id, + sizeof (ph->provider_legitimization_id), + "%llu", + (unsigned long long) process_row); + if ( (NULL != provider_legitimization_id) && + (0 != strcmp (provider_legitimization_id, + ph->provider_legitimization_id))) + { + GNUNET_break (0); + GNUNET_free (ph); + return NULL; + } + + ph->pd = pd; + ph->connection = connection; + ph->h_payto = *account_id; + ph->cb = cb; + ph->cb_cls = cb_cls; + code = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "code"); + if (NULL == code) + { + const char *err; + const char *desc; + const char *euri; + json_t *body; + + err = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "error"); + if (NULL == err) + { + GNUNET_break_op (0); + ph->status = TALER_KYCLOGIC_STATUS_USER_PENDING; + ph->http_status = MHD_HTTP_BAD_REQUEST; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("message", + "'code' parameter malformed"), + TALER_JSON_pack_ec ( + TALER_EC_GENERIC_PARAMETER_MALFORMED)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-bad-request", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); + return ph; + } + desc = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "error_description"); + euri = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "error_uri"); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "OAuth2 process %llu failed with error `%s'\n", + (unsigned long long) process_row, + err); + if (0 == strcmp (err, + "server_error")) + ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; + else if (0 == strcmp (err, + "unauthorized_client")) + ph->status = TALER_KYCLOGIC_STATUS_FAILED; + else if (0 == strcmp (err, + "temporarily_unavailable")) + ph->status = TALER_KYCLOGIC_STATUS_PENDING; + else + ph->status = TALER_KYCLOGIC_STATUS_INTERNAL_ERROR; + ph->http_status = MHD_HTTP_FORBIDDEN; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("error", + err), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("error_details", + desc)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("error_uri", + euri))); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-authentication-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); + return ph; + + } + + ph->eh = curl_easy_init (); + GNUNET_assert (NULL != ph->eh); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Requesting OAuth 2.0 data via HTTP POST `%s'\n", + pd->token_url); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (ph->eh, + CURLOPT_URL, + pd->token_url)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (ph->eh, + CURLOPT_VERBOSE, + 1)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (ph->eh, + CURLOPT_POST, + 1)); + { + char *client_id; + char *client_secret; + char *authorization_code; + char *redirect_uri_encoded; + char *hps; + + hps = GNUNET_STRINGS_data_to_string_alloc (&ph->h_payto, + sizeof (ph->h_payto)); + { + char *redirect_uri; + + GNUNET_asprintf (&redirect_uri, + "%skyc-proof/%s", + ps->exchange_base_url, + pd->section); + redirect_uri_encoded = TALER_urlencode (redirect_uri); + GNUNET_free (redirect_uri); + } + GNUNET_assert (NULL != redirect_uri_encoded); + client_id = curl_easy_escape (ph->eh, + pd->client_id, + 0); + GNUNET_assert (NULL != client_id); + client_secret = curl_easy_escape (ph->eh, + pd->client_secret, + 0); + GNUNET_assert (NULL != client_secret); + authorization_code = curl_easy_escape (ph->eh, + code, + 0); + GNUNET_assert (NULL != authorization_code); + GNUNET_asprintf (&ph->post_body, + "client_id=%s&redirect_uri=%s&state=%s&client_secret=%s&code=%s&grant_type=authorization_code", + client_id, + redirect_uri_encoded, + hps, + client_secret, + authorization_code); + curl_free (authorization_code); + curl_free (client_secret); + GNUNET_free (redirect_uri_encoded); + GNUNET_free (hps); + curl_free (client_id); + } + GNUNET_assert (CURLE_OK == + curl_easy_setopt (ph->eh, + CURLOPT_POSTFIELDS, + ph->post_body)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (ph->eh, + CURLOPT_FOLLOWLOCATION, + 1L)); + /* limit MAXREDIRS to 5 as a simple security measure against + a potential infinite loop caused by a malicious target */ + GNUNET_assert (CURLE_OK == + curl_easy_setopt (ph->eh, + CURLOPT_MAXREDIRS, + 5L)); + + ph->job = GNUNET_CURL_job_add (ps->curl_ctx, + ph->eh, + &handle_curl_login_finished, + ph); + return ph; +} + + +/** + * Function to asynchronously return the 404 not found + * page for the webhook. + * + * @param cls the `struct TALER_KYCLOGIC_WebhookHandle *` + */ +static void +wh_return_not_found (void *cls) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + struct MHD_Response *response; + + wh->task = NULL; + response = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + wh->cb (wh->cb_cls, + 0LLU, + NULL, + NULL, + NULL, + NULL, + TALER_KYCLOGIC_STATUS_KEEP, + GNUNET_TIME_UNIT_ZERO_ABS, + NULL, + MHD_HTTP_NOT_FOUND, + response); + GNUNET_free (wh); +} + + +/** + * Check KYC status and return result for Webhook. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param plc callback to lookup accounts with + * @param plc_cls closure for @a plc + * @param http_method HTTP method used for the webhook + * @param url_path rest of the URL after `/kyc-webhook/$LOGIC/`, as NULL-terminated array + * @param connection MHD connection object (for HTTP headers) + * @param body HTTP request body, or NULL if not available + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_WebhookHandle * +oauth2_webhook (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + TALER_KYCLOGIC_ProviderLookupCallback plc, + void *plc_cls, + const char *http_method, + const char *const url_path[], + struct MHD_Connection *connection, + const json_t *body, + TALER_KYCLOGIC_WebhookCallback cb, + void *cb_cls) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_WebhookHandle *wh; + + (void) pd; + (void) plc; + (void) plc_cls; + (void) http_method; + (void) url_path; + (void) connection; + (void) body; + wh = GNUNET_new (struct TALER_KYCLOGIC_WebhookHandle); + wh->cb = cb; + wh->cb_cls = cb_cls; + wh->ps = ps; + wh->task = GNUNET_SCHEDULER_add_now (&wh_return_not_found, + wh); + return wh; +} + + +/** + * Cancel KYC webhook execution. + * + * @param[in] wh handle of operation to cancel + */ +static void +oauth2_webhook_cancel (struct TALER_KYCLOGIC_WebhookHandle *wh) +{ + GNUNET_SCHEDULER_cancel (wh->task); + GNUNET_free (wh); +} + + +/** + * Initialize OAuth2.0 KYC logic plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct TALER_KYCLOGIC_Plugin` + */ +void * +libtaler_plugin_kyclogic_oauth2_init (void *cls) +{ + const struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct TALER_KYCLOGIC_Plugin *plugin; + struct PluginState *ps; + + ps = GNUNET_new (struct PluginState); + ps->cfg = cfg; + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "exchange", + "BASE_URL", + &ps->exchange_base_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "BASE_URL"); + GNUNET_free (ps); + return NULL; + } + ps->curl_ctx + = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &ps->curl_rc); + if (NULL == ps->curl_ctx) + { + GNUNET_break (0); + GNUNET_free (ps->exchange_base_url); + GNUNET_free (ps); + return NULL; + } + ps->curl_rc = GNUNET_CURL_gnunet_rc_create (ps->curl_ctx); + + plugin = GNUNET_new (struct TALER_KYCLOGIC_Plugin); + plugin->cls = ps; + plugin->load_configuration + = &oauth2_load_configuration; + plugin->unload_configuration + = &oauth2_unload_configuration; + plugin->initiate + = &oauth2_initiate; + plugin->initiate_cancel + = &oauth2_initiate_cancel; + plugin->proof + = &oauth2_proof; + plugin->proof_cancel + = &oauth2_proof_cancel; + plugin->webhook + = &oauth2_webhook; + plugin->webhook_cancel + = &oauth2_webhook_cancel; + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct TALER_KYCLOGIC_Plugin` + * @return NULL (always) + */ +void * +libtaler_plugin_kyclogic_oauth2_done (void *cls) +{ + struct TALER_KYCLOGIC_Plugin *plugin = cls; + struct PluginState *ps = plugin->cls; + + if (NULL != ps->curl_ctx) + { + GNUNET_CURL_fini (ps->curl_ctx); + ps->curl_ctx = NULL; + } + if (NULL != ps->curl_rc) + { + GNUNET_CURL_gnunet_rc_destroy (ps->curl_rc); + ps->curl_rc = NULL; + } + GNUNET_free (ps->exchange_base_url); + GNUNET_free (ps); + GNUNET_free (plugin); + return NULL; +} diff --git a/src/kyclogic/plugin_kyclogic_persona.c b/src/kyclogic/plugin_kyclogic_persona.c new file mode 100644 index 000000000..c68b7f881 --- /dev/null +++ b/src/kyclogic/plugin_kyclogic_persona.c @@ -0,0 +1,2268 @@ +/* + This file is part of GNU Taler + Copyright (C) 2022, 2023 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.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file plugin_kyclogic_persona.c + * @brief persona for an authentication flow logic + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_attributes.h" +#include "taler_kyclogic_plugin.h" +#include "taler_mhd_lib.h" +#include "taler_curl_lib.h" +#include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" +#include "taler_templating_lib.h" +#include <regex.h> +#include "taler_util.h" + + +/** + * Which version of the persona API are we implementing? + */ +#define PERSONA_VERSION "2021-07-05" + +/** + * Saves the state of a plugin. + */ +struct PluginState +{ + + /** + * Our base URL. + */ + char *exchange_base_url; + + /** + * Our global configuration. + */ + const struct GNUNET_CONFIGURATION_Handle *cfg; + + /** + * Context for CURL operations (useful to the event loop) + */ + struct GNUNET_CURL_Context *curl_ctx; + + /** + * Context for integrating @e curl_ctx with the + * GNUnet event loop. + */ + struct GNUNET_CURL_RescheduleContext *curl_rc; + + /** + * Authorization token to use when receiving webhooks from the Persona + * service. Optional. Note that webhooks are *global* and not per + * template. + */ + char *webhook_token; + + +}; + + +/** + * Keeps the plugin-specific state for + * a given configuration section. + */ +struct TALER_KYCLOGIC_ProviderDetails +{ + + /** + * Overall plugin state. + */ + struct PluginState *ps; + + /** + * Configuration section that configured us. + */ + char *section; + + /** + * Salt to use for idempotency. + */ + char *salt; + + /** + * Authorization token to use when talking + * to the service. + */ + char *auth_token; + + /** + * Template ID for the KYC check to perform. + */ + char *template_id; + + /** + * Subdomain to use. + */ + char *subdomain; + + /** + * Name of the program we use to convert outputs + * from Persona into our JSON inputs. + */ + char *conversion_binary; + + /** + * Where to redirect the client upon completion. + */ + char *post_kyc_redirect_url; + + /** + * Validity time for a successful KYC process. + */ + struct GNUNET_TIME_Relative validity; + + /** + * Curl-ready authentication header to use. + */ + struct curl_slist *slist; + +}; + + +/** + * Handle for an initiation operation. + */ +struct TALER_KYCLOGIC_InitiateHandle +{ + + /** + * Hash of the payto:// URI we are initiating the KYC for. + */ + struct TALER_PaytoHashP h_payto; + + /** + * UUID being checked. + */ + uint64_t legitimization_uuid; + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Continuation to call. + */ + TALER_KYCLOGIC_InitiateCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * URL of the cURL request. + */ + char *url; + + /** + * Request-specific headers to use. + */ + struct curl_slist *slist; + +}; + + +/** + * Handle for an KYC proof operation. + */ +struct TALER_KYCLOGIC_ProofHandle +{ + + /** + * Overall plugin state. + */ + struct PluginState *ps; + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Continuation to call. + */ + TALER_KYCLOGIC_ProofCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Task for asynchronous execution. + */ + struct GNUNET_SCHEDULER_Task *task; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * URL of the cURL request. + */ + char *url; + + /** + * Handle to an external process that converts the + * Persona response to our internal format. + */ + struct TALER_JSON_ExternalConversion *ec; + + /** + * Hash of the payto:// URI we are checking the KYC for. + */ + struct TALER_PaytoHashP h_payto; + + /** + * Row in the legitimization processes of the + * legitimization proof that is being checked. + */ + uint64_t process_row; + + /** + * Account ID at the provider. + */ + char *provider_user_id; + + /** + * Account ID from the service. + */ + char *account_id; + + /** + * Inquiry ID at the provider. + */ + char *inquiry_id; +}; + + +/** + * Handle for an KYC Web hook operation. + */ +struct TALER_KYCLOGIC_WebhookHandle +{ + + /** + * Continuation to call when done. + */ + TALER_KYCLOGIC_WebhookCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Task for asynchronous execution. + */ + struct GNUNET_SCHEDULER_Task *task; + + /** + * Overall plugin state. + */ + struct PluginState *ps; + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Verification ID from the service. + */ + char *inquiry_id; + + /** + * Account ID from the service. + */ + char *account_id; + + /** + * URL of the cURL request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Response to return asynchronously. + */ + struct MHD_Response *resp; + + /** + * ID of the template the webhook is about, + * according to the service. + */ + const char *template_id; + + /** + * Handle to an external process that converts the + * Persona response to our internal format. + */ + struct TALER_JSON_ExternalConversion *ec; + + /** + * Our account ID. + */ + struct TALER_PaytoHashP h_payto; + + /** + * UUID being checked. + */ + uint64_t process_row; + + /** + * HTTP response code to return asynchronously. + */ + unsigned int response_code; +}; + + +/** + * Release configuration resources previously loaded + * + * @param[in] pd configuration to release + */ +static void +persona_unload_configuration (struct TALER_KYCLOGIC_ProviderDetails *pd) +{ + curl_slist_free_all (pd->slist); + GNUNET_free (pd->auth_token); + GNUNET_free (pd->template_id); + GNUNET_free (pd->subdomain); + GNUNET_free (pd->conversion_binary); + GNUNET_free (pd->salt); + GNUNET_free (pd->section); + GNUNET_free (pd->post_kyc_redirect_url); + GNUNET_free (pd); +} + + +/** + * Load the configuration of the KYC provider. + * + * @param cls closure + * @param provider_section_name configuration section to parse + * @return NULL if configuration is invalid + */ +static struct TALER_KYCLOGIC_ProviderDetails * +persona_load_configuration (void *cls, + const char *provider_section_name) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_ProviderDetails *pd; + + pd = GNUNET_new (struct TALER_KYCLOGIC_ProviderDetails); + pd->ps = ps; + pd->section = GNUNET_strdup (provider_section_name); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (ps->cfg, + provider_section_name, + "KYC_PERSONA_VALIDITY", + &pd->validity)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_PERSONA_VALIDITY"); + persona_unload_configuration (pd); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_PERSONA_AUTH_TOKEN", + &pd->auth_token)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_PERSONA_AUTH_TOKEN"); + persona_unload_configuration (pd); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_PERSONA_SALT", + &pd->salt)) + { + uint32_t salt[8]; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + salt, + sizeof (salt)); + pd->salt = GNUNET_STRINGS_data_to_string_alloc (salt, + sizeof (salt)); + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_PERSONA_SUBDOMAIN", + &pd->subdomain)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_PERSONA_SUBDOMAIN"); + persona_unload_configuration (pd); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_PERSONA_CONVERTER_HELPER", + &pd->conversion_binary)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_PERSONA_CONVERTER_HELPER"); + persona_unload_configuration (pd); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_PERSONA_POST_URL", + &pd->post_kyc_redirect_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_PERSONA_POST_URL"); + persona_unload_configuration (pd); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_PERSONA_TEMPLATE_ID", + &pd->template_id)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_PERSONA_TEMPLATE_ID"); + persona_unload_configuration (pd); + return NULL; + } + { + char *auth; + + GNUNET_asprintf (&auth, + "%s: Bearer %s", + MHD_HTTP_HEADER_AUTHORIZATION, + pd->auth_token); + pd->slist = curl_slist_append (NULL, + auth); + GNUNET_free (auth); + GNUNET_asprintf (&auth, + "%s: %s", + MHD_HTTP_HEADER_ACCEPT, + "application/json"); + pd->slist = curl_slist_append (pd->slist, + "Persona-Version: " + PERSONA_VERSION); + GNUNET_free (auth); + } + return pd; +} + + +/** + * Cancel KYC check initiation. + * + * @param[in] ih handle of operation to cancel + */ +static void +persona_initiate_cancel (struct TALER_KYCLOGIC_InitiateHandle *ih) +{ + if (NULL != ih->job) + { + GNUNET_CURL_job_cancel (ih->job); + ih->job = NULL; + } + GNUNET_free (ih->url); + TALER_curl_easy_post_finished (&ih->ctx); + curl_slist_free_all (ih->slist); + GNUNET_free (ih); +} + + +/** + * Function called when we're done processing the + * HTTP POST "/api/v1/inquiries" request. + * + * @param cls the `struct TALER_KYCLOGIC_InitiateHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_initiate_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_KYCLOGIC_InitiateHandle *ih = cls; + const struct TALER_KYCLOGIC_ProviderDetails *pd = ih->pd; + const json_t *j = response; + char *url; + json_t *data; + const char *type; + const char *inquiry_id; + const char *persona_account_id; + const char *ename; + unsigned int eline; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_string ("id", + &inquiry_id), + GNUNET_JSON_spec_end () + }; + + ih->job = NULL; + switch (response_code) + { + case MHD_HTTP_CREATED: + /* handled below */ + break; + case MHD_HTTP_UNAUTHORIZED: + case MHD_HTTP_FORBIDDEN: + { + const char *msg; + + msg = json_string_value ( + json_object_get ( + json_array_get ( + json_object_get (j, + "errors"), + 0), + "title")); + + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED, + NULL, + NULL, + NULL, + msg); + persona_initiate_cancel (ih); + return; + } + case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_CONFLICT: + { + const char *msg; + + msg = json_string_value ( + json_object_get ( + json_array_get ( + json_object_get (j, + "errors"), + 0), + "title")); + + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY, + NULL, + NULL, + NULL, + msg); + persona_initiate_cancel (ih); + return; + } + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_UNPROCESSABLE_ENTITY: + { + const char *msg; + + GNUNET_break (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + msg = json_string_value ( + json_object_get ( + json_array_get ( + json_object_get (j, + "errors"), + 0), + "title")); + + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_BUG, + NULL, + NULL, + NULL, + msg); + persona_initiate_cancel (ih); + return; + } + case MHD_HTTP_TOO_MANY_REQUESTS: + { + const char *msg; + + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Rate limiting requested:\n"); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + msg = json_string_value ( + json_object_get ( + json_array_get ( + json_object_get (j, + "errors"), + 0), + "title")); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED, + NULL, + NULL, + NULL, + msg); + persona_initiate_cancel (ih); + return; + } + default: + { + char *err; + + GNUNET_break_op (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + GNUNET_asprintf (&err, + "Unexpected HTTP status %u from Persona\n", + (unsigned int) response_code); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY, + NULL, + NULL, + NULL, + err); + GNUNET_free (err); + persona_initiate_cancel (ih); + return; + } + } + data = json_object_get (j, + "data"); + if (NULL == data) + { + GNUNET_break_op (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + persona_initiate_cancel (ih); + return; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (data, + spec, + &ename, + &eline)) + { + GNUNET_break_op (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY, + NULL, + NULL, + NULL, + ename); + persona_initiate_cancel (ih); + return; + } + persona_account_id + = json_string_value ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get (data, + "relationships"), + "account"), + "data"), + "id")); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting inquiry %s for Persona account %s\n", + inquiry_id, + persona_account_id); + GNUNET_asprintf (&url, + "https://%s.withpersona.com/verify" + "?inquiry-id=%s", + pd->subdomain, + inquiry_id); + ih->cb (ih->cb_cls, + TALER_EC_NONE, + url, + persona_account_id, + inquiry_id, + NULL); + GNUNET_free (url); + persona_initiate_cancel (ih); +} + + +/** + * Initiate KYC check. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param account_id which account to trigger process for + * @param legitimization_uuid unique ID for the legitimization process + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_InitiateHandle * +persona_initiate (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + const struct TALER_PaytoHashP *account_id, + uint64_t legitimization_uuid, + TALER_KYCLOGIC_InitiateCallback cb, + void *cb_cls) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_InitiateHandle *ih; + json_t *body; + CURL *eh; + + eh = curl_easy_init (); + if (NULL == eh) + { + GNUNET_break (0); + return NULL; + } + ih = GNUNET_new (struct TALER_KYCLOGIC_InitiateHandle); + ih->legitimization_uuid = legitimization_uuid; + ih->cb = cb; + ih->cb_cls = cb_cls; + ih->h_payto = *account_id; + ih->pd = pd; + GNUNET_asprintf (&ih->url, + "https://withpersona.com/api/v1/inquiries"); + { + char *payto_s; + char *proof_url; + char ref_s[24]; + + GNUNET_snprintf (ref_s, + sizeof (ref_s), + "%llu", + (unsigned long long) ih->legitimization_uuid); + payto_s = GNUNET_STRINGS_data_to_string_alloc (&ih->h_payto, + sizeof (ih->h_payto)); + GNUNET_break ('/' == + pd->ps->exchange_base_url[strlen ( + pd->ps->exchange_base_url) - 1]); + GNUNET_asprintf (&proof_url, + "%skyc-proof/%s?state=%s", + pd->ps->exchange_base_url, + pd->section, + payto_s); + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_steal ( + "data", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_steal ( + "attributes", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("inquiry_template_id", + pd->template_id), + GNUNET_JSON_pack_string ("reference_id", + ref_s), + GNUNET_JSON_pack_string ("redirect_uri", + proof_url) + ))))); + GNUNET_assert (NULL != body); + GNUNET_free (payto_s); + GNUNET_free (proof_url); + } + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_VERBOSE, + 0)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_MAXREDIRS, + 1L)); + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + ih->url)); + ih->ctx.disable_compression = true; + if (GNUNET_OK != + TALER_curl_easy_post (&ih->ctx, + eh, + body)) + { + GNUNET_break (0); + GNUNET_free (ih->url); + GNUNET_free (ih); + curl_easy_cleanup (eh); + json_decref (body); + return NULL; + } + json_decref (body); + ih->job = GNUNET_CURL_job_add2 (ps->curl_ctx, + eh, + ih->ctx.headers, + &handle_initiate_finished, + ih); + GNUNET_CURL_extend_headers (ih->job, + pd->slist); + { + char *ikh; + + GNUNET_asprintf (&ikh, + "Idempotency-Key: %llu-%s", + (unsigned long long) ih->legitimization_uuid, + pd->salt); + ih->slist = curl_slist_append (NULL, + ikh); + GNUNET_free (ikh); + } + GNUNET_CURL_extend_headers (ih->job, + ih->slist); + return ih; +} + + +/** + * Cancel KYC proof. + * + * @param[in] ph handle of operation to cancel + */ +static void +persona_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph) +{ + if (NULL != ph->job) + { + GNUNET_CURL_job_cancel (ph->job); + ph->job = NULL; + } + if (NULL != ph->ec) + { + TALER_JSON_external_conversion_stop (ph->ec); + ph->ec = NULL; + } + GNUNET_free (ph->url); + GNUNET_free (ph->provider_user_id); + GNUNET_free (ph->account_id); + GNUNET_free (ph->inquiry_id); + GNUNET_free (ph); +} + + +/** + * Call @a ph callback with the operation result. + * + * @param ph proof handle to generate reply for + * @param status status to return + * @param account_id account to return + * @param inquiry_id inquiry ID to supply + * @param http_status HTTP status to use + * @param template template to instantiate + * @param[in] body body for the template to use (reference + * is consumed) + */ +static void +proof_generic_reply (struct TALER_KYCLOGIC_ProofHandle *ph, + enum TALER_KYCLOGIC_KycStatus status, + const char *account_id, + const char *inquiry_id, + unsigned int http_status, + const char *template, + json_t *body) +{ + struct MHD_Response *resp; + enum GNUNET_GenericReturnValue ret; + + /* This API is not usable for successful replies */ + GNUNET_assert (TALER_KYCLOGIC_STATUS_SUCCESS != status); + ret = TALER_TEMPLATING_build (ph->connection, + &http_status, + template, + NULL, + NULL, + body, + &resp); + json_decref (body); + if (GNUNET_SYSERR == ret) + { + GNUNET_break (0); + resp = NULL; /* good luck */ + } + ph->cb (ph->cb_cls, + status, + account_id, + inquiry_id, + GNUNET_TIME_UNIT_ZERO_ABS, + NULL, + http_status, + resp); +} + + +/** + * Call @a ph callback with HTTP error response. + * + * @param ph proof handle to generate reply for + * @param inquiry_id inquiry ID to supply + * @param http_status HTTP status to use + * @param template template to instantiate + * @param[in] body body for the template to use (reference + * is consumed) + */ +static void +proof_reply_error (struct TALER_KYCLOGIC_ProofHandle *ph, + const char *inquiry_id, + unsigned int http_status, + const char *template, + json_t *body) +{ + proof_generic_reply (ph, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + NULL, /* user id */ + inquiry_id, + http_status, + template, + body); +} + + +/** + * Return a response for the @a ph request indicating a + * protocol violation by the Persona server. + * + * @param[in,out] ph request we are processing + * @param response_code HTTP status returned by Persona + * @param inquiry_id ID of the inquiry this is about + * @param detail where the response was wrong + * @param data full response data to output + */ +static void +return_invalid_response (struct TALER_KYCLOGIC_ProofHandle *ph, + unsigned int response_code, + const char *inquiry_id, + const char *detail, + const json_t *data) +{ + proof_reply_error ( + ph, + inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-invalid-response", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + GNUNET_JSON_pack_string ("persona_inquiry_id", + inquiry_id), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), + GNUNET_JSON_pack_string ("detail", + detail), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); +} + + +/** + * Start the external conversion helper. + * + * @param pd configuration details + * @param attr attributes to give to the helper + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle for the helper + */ +static struct TALER_JSON_ExternalConversion * +start_conversion (const struct TALER_KYCLOGIC_ProviderDetails *pd, + const json_t *attr, + TALER_JSON_JsonCallback cb, + void *cb_cls) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Calling converter `%s' with JSON\n", + pd->conversion_binary); + json_dumpf (attr, + stderr, + JSON_INDENT (2)); + return TALER_JSON_external_conversion_start ( + attr, + cb, + cb_cls, + pd->conversion_binary, + pd->conversion_binary, + "-a", + pd->auth_token, + NULL + ); +} + + +/** + * Type of a callback that receives a JSON @a result. + * + * @param cls closure with a `struct TALER_KYCLOGIC_ProofHandle *` + * @param status_type how did the process die + * @param code termination status code from the process + * @param attr result some JSON result, NULL if we failed to get an JSON output + */ +static void +proof_post_conversion_cb (void *cls, + enum GNUNET_OS_ProcessStatusType status_type, + unsigned long code, + const json_t *attr) +{ + struct TALER_KYCLOGIC_ProofHandle *ph = cls; + struct MHD_Response *resp; + struct GNUNET_TIME_Absolute expiration; + + ph->ec = NULL; + if ( (NULL == attr) || + (0 != code) ) + { + GNUNET_break_op (0); + return_invalid_response (ph, + MHD_HTTP_OK, + ph->inquiry_id, + "converter", + NULL); + persona_proof_cancel (ph); + return; + } + expiration = GNUNET_TIME_relative_to_absolute (ph->pd->validity); + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_LOCATION, + ph->pd->post_kyc_redirect_url)); + TALER_MHD_add_global_headers (resp); + ph->cb (ph->cb_cls, + TALER_KYCLOGIC_STATUS_SUCCESS, + ph->account_id, + ph->inquiry_id, + expiration, + attr, + MHD_HTTP_SEE_OTHER, + resp); + persona_proof_cancel (ph); +} + + +/** + * Function called when we're done processing the + * HTTP "/api/v1/inquiries/{inquiry-id}" request. + * + * @param cls the `struct TALER_KYCLOGIC_InitiateHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_proof_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_KYCLOGIC_ProofHandle *ph = cls; + const json_t *j = response; + const json_t *data = json_object_get (j, + "data"); + + ph->job = NULL; + switch (response_code) + { + case MHD_HTTP_OK: + { + const char *inquiry_id; + const char *account_id; + const char *type = NULL; + const json_t *attributes; + const json_t *relationships; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_string ("id", + &inquiry_id), + GNUNET_JSON_spec_object_const ("attributes", + &attributes), + GNUNET_JSON_spec_object_const ("relationships", + &relationships), + GNUNET_JSON_spec_end () + }; + + if ( (NULL == data) || + (GNUNET_OK != + GNUNET_JSON_parse (data, + spec, + NULL, NULL)) || + (0 != strcmp (type, + "inquiry")) ) + { + GNUNET_break_op (0); + return_invalid_response (ph, + response_code, + inquiry_id, + "data", + data); + break; + } + + { + const char *status; /* "completed", what else? */ + const char *reference_id; /* or legitimization number */ + const char *expired_at = NULL; /* often 'null' format: "2022-08-18T10:14:26.000Z" */ + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("status", + &status), + GNUNET_JSON_spec_string ("reference-id", + &reference_id), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("expired-at", + &expired_at), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (attributes, + ispec, + NULL, NULL)) + { + GNUNET_break_op (0); + return_invalid_response (ph, + response_code, + inquiry_id, + "data-attributes", + data); + break; + } + { + unsigned long long idr; + char dummy; + + if ( (1 != sscanf (reference_id, + "%llu%c", + &idr, + &dummy)) || + (idr != ph->process_row) ) + { + GNUNET_break_op (0); + return_invalid_response (ph, + response_code, + inquiry_id, + "data-attributes-reference_id", + data); + break; + } + } + + if (0 != strcmp (inquiry_id, + ph->inquiry_id)) + { + GNUNET_break_op (0); + return_invalid_response (ph, + response_code, + inquiry_id, + "data-id", + data); + break; + } + + account_id = json_string_value ( + json_object_get ( + json_object_get ( + json_object_get ( + relationships, + "account"), + "data"), + "id")); + + if (0 != strcmp (status, + "completed")) + { + proof_generic_reply ( + ph, + TALER_KYCLOGIC_STATUS_FAILED, + account_id, + inquiry_id, + MHD_HTTP_OK, + "persona-kyc-failed", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + GNUNET_JSON_pack_string ("persona_inquiry_id", + inquiry_id), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); + break; + } + + if (NULL == account_id) + { + GNUNET_break_op (0); + return_invalid_response (ph, + response_code, + inquiry_id, + "data-relationships-account-data-id", + data); + break; + } + ph->account_id = GNUNET_strdup (account_id); + ph->ec = start_conversion (ph->pd, + j, + &proof_post_conversion_cb, + ph); + if (NULL == ph->ec) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start Persona conversion helper\n"); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-logic-failure", + GNUNET_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_GENERIC_KYC_CONVERTER_FAILED))); + break; + } + } + return; /* continued in proof_post_conversion_cb */ + } + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_CONFLICT: + case MHD_HTTP_UNPROCESSABLE_ENTITY: + /* These are errors with this code */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-logic-failure", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), + + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); + break; + case MHD_HTTP_UNAUTHORIZED: + /* These are failures of the exchange operator */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refused access with HTTP status code %u\n", + (unsigned int) response_code); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-exchange-unauthorized", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); + break; + case MHD_HTTP_PAYMENT_REQUIRED: + /* These are failures of the exchange operator */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refused access with HTTP status code %u\n", + (unsigned int) response_code); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_SERVICE_UNAVAILABLE, + "persona-exchange-unpaid", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); + break; + case MHD_HTTP_REQUEST_TIMEOUT: + /* These are networking issues */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_GATEWAY_TIMEOUT, + "persona-network-timeout", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); + break; + case MHD_HTTP_TOO_MANY_REQUESTS: + /* This is a load issue */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_SERVICE_UNAVAILABLE, + "persona-load-failure", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* This is an issue with Persona */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-provider-failure", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_ERROR), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); + break; + default: + /* This is an issue with Persona */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-invalid-response", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); + break; + } + persona_proof_cancel (ph); +} + + +/** + * Check KYC status and return final result to human. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param connection MHD connection object (for HTTP headers) + * @param account_id which account to trigger process for + * @param process_row row in the legitimization processes table the legitimization is for + * @param provider_user_id user ID (or NULL) the proof is for + * @param inquiry_id legitimization ID the proof is for + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_ProofHandle * +persona_proof (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + struct MHD_Connection *connection, + const struct TALER_PaytoHashP *account_id, + uint64_t process_row, + const char *provider_user_id, + const char *inquiry_id, + TALER_KYCLOGIC_ProofCallback cb, + void *cb_cls) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_ProofHandle *ph; + CURL *eh; + + eh = curl_easy_init (); + if (NULL == eh) + { + GNUNET_break (0); + return NULL; + } + ph = GNUNET_new (struct TALER_KYCLOGIC_ProofHandle); + ph->ps = ps; + ph->pd = pd; + ph->cb = cb; + ph->cb_cls = cb_cls; + ph->connection = connection; + ph->process_row = process_row; + ph->h_payto = *account_id; + /* Note: we do not expect this to be non-NULL */ + if (NULL != provider_user_id) + ph->provider_user_id = GNUNET_strdup (provider_user_id); + if (NULL != inquiry_id) + ph->inquiry_id = GNUNET_strdup (inquiry_id); + GNUNET_asprintf (&ph->url, + "https://withpersona.com/api/v1/inquiries/%s", + inquiry_id); + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_VERBOSE, + 0)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_MAXREDIRS, + 1L)); + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + ph->url)); + ph->job = GNUNET_CURL_job_add2 (ps->curl_ctx, + eh, + pd->slist, + &handle_proof_finished, + ph); + return ph; +} + + +/** + * Cancel KYC webhook execution. + * + * @param[in] wh handle of operation to cancel + */ +static void +persona_webhook_cancel (struct TALER_KYCLOGIC_WebhookHandle *wh) +{ + if (NULL != wh->task) + { + GNUNET_SCHEDULER_cancel (wh->task); + wh->task = NULL; + } + if (NULL != wh->job) + { + GNUNET_CURL_job_cancel (wh->job); + wh->job = NULL; + } + if (NULL != wh->ec) + { + TALER_JSON_external_conversion_stop (wh->ec); + wh->ec = NULL; + } + GNUNET_free (wh->account_id); + GNUNET_free (wh->inquiry_id); + GNUNET_free (wh->url); + GNUNET_free (wh); +} + + +/** + * Call @a wh callback with the operation result. + * + * @param wh proof handle to generate reply for + * @param status status to return + * @param account_id account to return + * @param inquiry_id inquiry ID to supply + * @param attr KYC attribute data for the client + * @param http_status HTTP status to use + */ +static void +webhook_generic_reply (struct TALER_KYCLOGIC_WebhookHandle *wh, + enum TALER_KYCLOGIC_KycStatus status, + const char *account_id, + const char *inquiry_id, + const json_t *attr, + unsigned int http_status) +{ + struct MHD_Response *resp; + struct GNUNET_TIME_Absolute expiration; + + if (TALER_KYCLOGIC_STATUS_SUCCESS == status) + expiration = GNUNET_TIME_relative_to_absolute (wh->pd->validity); + else + expiration = GNUNET_TIME_UNIT_ZERO_ABS; + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + account_id, + inquiry_id, + status, + expiration, + attr, + http_status, + resp); +} + + +/** + * Call @a wh callback with HTTP error response. + * + * @param wh proof handle to generate reply for + * @param inquiry_id inquiry ID to supply + * @param http_status HTTP status to use + */ +static void +webhook_reply_error (struct TALER_KYCLOGIC_WebhookHandle *wh, + const char *inquiry_id, + unsigned int http_status) +{ + webhook_generic_reply (wh, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + NULL, /* user id */ + inquiry_id, + NULL, /* attributes */ + http_status); +} + + +/** + * Type of a callback that receives a JSON @a result. + * + * @param cls closure with a `struct TALER_KYCLOGIC_WebhookHandle *` + * @param status_type how did the process die + * @param code termination status code from the process + * @param attr some JSON result, NULL if we failed to get an JSON output + */ +static void +webhook_post_conversion_cb (void *cls, + enum GNUNET_OS_ProcessStatusType status_type, + unsigned long code, + const json_t *attr) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + + wh->ec = NULL; + webhook_generic_reply (wh, + TALER_KYCLOGIC_STATUS_SUCCESS, + wh->account_id, + wh->inquiry_id, + attr, + MHD_HTTP_OK); +} + + +/** + * Function called when we're done processing the + * HTTP "/api/v1/inquiries/{inquiry_id}" request. + * + * @param cls the `struct TALER_KYCLOGIC_WebhookHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_webhook_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + const json_t *j = response; + const json_t *data = json_object_get (j, + "data"); + + wh->job = NULL; + switch (response_code) + { + case MHD_HTTP_OK: + { + const char *inquiry_id; + const char *account_id; + const char *type = NULL; + const json_t *attributes; + const json_t *relationships; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_string ("id", + &inquiry_id), + GNUNET_JSON_spec_object_const ("attributes", + &attributes), + GNUNET_JSON_spec_object_const ("relationships", + &relationships), + GNUNET_JSON_spec_end () + }; + + if ( (NULL == data) || + (GNUNET_OK != + GNUNET_JSON_parse (data, + spec, + NULL, NULL)) || + (0 != strcmp (type, + "inquiry")) ) + { + GNUNET_break_op (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_BAD_GATEWAY); + break; + } + + { + const char *status; /* "completed", what else? */ + const char *reference_id; /* or legitimization number */ + const char *expired_at = NULL; /* often 'null' format: "2022-08-18T10:14:26.000Z" */ + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("status", + &status), + GNUNET_JSON_spec_string ("reference-id", + &reference_id), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("expired-at", + &expired_at), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (attributes, + ispec, + NULL, NULL)) + { + GNUNET_break_op (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_BAD_GATEWAY); + break; + } + { + unsigned long long idr; + char dummy; + + if ( (1 != sscanf (reference_id, + "%llu%c", + &idr, + &dummy)) || + (idr != wh->process_row) ) + { + GNUNET_break_op (0); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_BAD_GATEWAY); + break; + } + } + + if (0 != strcmp (inquiry_id, + wh->inquiry_id)) + { + GNUNET_break_op (0); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_BAD_GATEWAY); + break; + } + + account_id = json_string_value ( + json_object_get ( + json_object_get ( + json_object_get ( + relationships, + "account"), + "data"), + "id")); + + if (0 != strcmp (status, + "completed")) + { + webhook_generic_reply (wh, + TALER_KYCLOGIC_STATUS_FAILED, + account_id, + inquiry_id, + NULL, + MHD_HTTP_OK); + break; + } + + if (NULL == account_id) + { + GNUNET_break_op (0); + json_dumpf (data, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_BAD_GATEWAY); + break; + } + wh->account_id = GNUNET_strdup (account_id); + wh->ec = start_conversion (wh->pd, + j, + &webhook_post_conversion_cb, + wh); + if (NULL == wh->ec) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start Persona conversion helper\n"); + webhook_reply_error (wh, + inquiry_id, + MHD_HTTP_INTERNAL_SERVER_ERROR); + break; + } + } + return; /* continued in webhook_post_conversion_cb */ + } + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_CONFLICT: + case MHD_HTTP_UNPROCESSABLE_ENTITY: + /* These are errors with this code */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_BAD_GATEWAY); + break; + case MHD_HTTP_UNAUTHORIZED: + /* These are failures of the exchange operator */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refused access with HTTP status code %u\n", + (unsigned int) response_code); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_INTERNAL_SERVER_ERROR); + break; + case MHD_HTTP_PAYMENT_REQUIRED: + /* These are failures of the exchange operator */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refused access with HTTP status code %u\n", + (unsigned int) response_code); + + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_INTERNAL_SERVER_ERROR); + break; + case MHD_HTTP_REQUEST_TIMEOUT: + /* These are networking issues */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_GATEWAY_TIMEOUT); + break; + case MHD_HTTP_TOO_MANY_REQUESTS: + /* This is a load issue */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_SERVICE_UNAVAILABLE); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* This is an issue with Persona */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_BAD_GATEWAY); + break; + default: + /* This is an issue with Persona */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "PERSONA failed with response %u:\n", + (unsigned int) response_code); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + webhook_reply_error (wh, + wh->inquiry_id, + MHD_HTTP_BAD_GATEWAY); + break; + } + + persona_webhook_cancel (wh); +} + + +/** + * Asynchronously return a reply for the webhook. + * + * @param cls a `struct TALER_KYCLOGIC_WebhookHandle *` + */ +static void +async_webhook_reply (void *cls) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + + wh->task = NULL; + wh->cb (wh->cb_cls, + wh->process_row, + (0 == wh->process_row) + ? NULL + : &wh->h_payto, + wh->pd->section, + NULL, + wh->inquiry_id, /* provider legi ID */ + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + wh->response_code, + wh->resp); + persona_webhook_cancel (wh); +} + + +/** + * Function called with the provider details and + * associated plugin closures for matching logics. + * + * @param cls closure + * @param pd provider details of a matching logic + * @param plugin_cls closure of the plugin + * @return #GNUNET_OK to continue to iterate + */ +static enum GNUNET_GenericReturnValue +locate_details_cb ( + void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + void *plugin_cls) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + + /* This type-checks 'pd' */ + GNUNET_assert (plugin_cls == wh->ps); + if (0 == strcmp (pd->template_id, + wh->template_id)) + { + wh->pd = pd; + return GNUNET_NO; + } + return GNUNET_OK; +} + + +/** + * Check KYC status and return result for Webhook. We do NOT implement the + * authentication check proposed by the PERSONA documentation, as it would + * allow an attacker who learns the access token to easily bypass the KYC + * checks. Instead, we insist on explicitly requesting the KYC status from the + * provider (at least on success). + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param plc callback to lookup accounts with + * @param plc_cls closure for @a plc + * @param http_method HTTP method used for the webhook + * @param url_path rest of the URL after `/kyc-webhook/` + * @param connection MHD connection object (for HTTP headers) + * @param body HTTP request body + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_WebhookHandle * +persona_webhook (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + TALER_KYCLOGIC_ProviderLookupCallback plc, + void *plc_cls, + const char *http_method, + const char *const url_path[], + struct MHD_Connection *connection, + const json_t *body, + TALER_KYCLOGIC_WebhookCallback cb, + void *cb_cls) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_WebhookHandle *wh; + CURL *eh; + enum GNUNET_DB_QueryStatus qs; + const char *persona_inquiry_id; + const char *auth_header; + + /* Persona webhooks are expected by logic, not by template */ + GNUNET_break_op (NULL == pd); + wh = GNUNET_new (struct TALER_KYCLOGIC_WebhookHandle); + wh->cb = cb; + wh->cb_cls = cb_cls; + wh->ps = ps; + wh->connection = connection; + wh->pd = pd; + auth_header = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_AUTHORIZATION); + if ( (NULL != ps->webhook_token) && + ( (NULL == auth_header) || + (0 != strcmp (ps->webhook_token, + auth_header)) ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid authorization header `%s' received for Persona webhook\n", + auth_header); + wh->resp = TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_WEBHOOK_UNAUTHORIZED), + GNUNET_JSON_pack_string ("detail", + "unexpected 'Authorization' header")); + wh->response_code = MHD_HTTP_UNAUTHORIZED; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + + wh->template_id + = json_string_value ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + body, + "data"), + "attributes"), + "payload"), + "data"), + "relationships"), + "inquiry-template"), + "data"), + "id")); + if (NULL == wh->template_id) + { + GNUNET_break_op (0); + json_dumpf (body, + stderr, + JSON_INDENT (2)); + wh->resp = TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), + GNUNET_JSON_pack_string ("detail", + "data-attributes-payload-data-id"), + GNUNET_JSON_pack_object_incref ("webhook_body", + (json_t *) body)); + wh->response_code = MHD_HTTP_BAD_REQUEST; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + TALER_KYCLOGIC_kyc_get_details ("persona", + &locate_details_cb, + wh); + if (NULL == wh->pd) + { + GNUNET_break_op (0); + json_dumpf (body, + stderr, + JSON_INDENT (2)); + wh->resp = TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN), + GNUNET_JSON_pack_string ("detail", + wh->template_id), + GNUNET_JSON_pack_object_incref ("webhook_body", + (json_t *) body)); + wh->response_code = MHD_HTTP_BAD_REQUEST; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + + persona_inquiry_id + = json_string_value ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + json_object_get ( + body, + "data"), + "attributes"), + "payload"), + "data"), + "id")); + if (NULL == persona_inquiry_id) + { + GNUNET_break_op (0); + json_dumpf (body, + stderr, + JSON_INDENT (2)); + wh->resp = TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), + GNUNET_JSON_pack_string ("detail", + "data-attributes-payload-data-id"), + GNUNET_JSON_pack_object_incref ("webhook_body", + (json_t *) body)); + wh->response_code = MHD_HTTP_BAD_REQUEST; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + qs = plc (plc_cls, + wh->pd->section, + persona_inquiry_id, + &wh->h_payto, + &wh->process_row); + if (qs < 0) + { + wh->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, + "provider-legitimization-lookup"); + wh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received Persona kyc-webhook for unknown verification ID `%s'\n", + persona_inquiry_id); + wh->resp = TALER_MHD_make_error ( + TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, + persona_inquiry_id); + wh->response_code = MHD_HTTP_NOT_FOUND; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + wh->inquiry_id = GNUNET_strdup (persona_inquiry_id); + + eh = curl_easy_init (); + if (NULL == eh) + { + GNUNET_break (0); + wh->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_ALLOCATION_FAILURE, + NULL); + wh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, + wh); + return wh; + } + + GNUNET_asprintf (&wh->url, + "https://withpersona.com/api/v1/inquiries/%s", + persona_inquiry_id); + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_VERBOSE, + 0)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_MAXREDIRS, + 1L)); + GNUNET_break (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + wh->url)); + wh->job = GNUNET_CURL_job_add2 (ps->curl_ctx, + eh, + wh->pd->slist, + &handle_webhook_finished, + wh); + return wh; +} + + +/** + * Initialize persona logic plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct TALER_KYCLOGIC_Plugin` + */ +void * +libtaler_plugin_kyclogic_persona_init (void *cls) +{ + const struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct TALER_KYCLOGIC_Plugin *plugin; + struct PluginState *ps; + + ps = GNUNET_new (struct PluginState); + ps->cfg = cfg; + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "exchange", + "BASE_URL", + &ps->exchange_base_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "BASE_URL"); + GNUNET_free (ps); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + "kyclogic-persona", + "WEBHOOK_AUTH_TOKEN", + &ps->webhook_token)) + { + /* optional */ + ps->webhook_token = NULL; + } + + ps->curl_ctx + = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &ps->curl_rc); + if (NULL == ps->curl_ctx) + { + GNUNET_break (0); + GNUNET_free (ps->exchange_base_url); + GNUNET_free (ps); + return NULL; + } + ps->curl_rc = GNUNET_CURL_gnunet_rc_create (ps->curl_ctx); + + plugin = GNUNET_new (struct TALER_KYCLOGIC_Plugin); + plugin->cls = ps; + plugin->load_configuration + = &persona_load_configuration; + plugin->unload_configuration + = &persona_unload_configuration; + plugin->initiate + = &persona_initiate; + plugin->initiate_cancel + = &persona_initiate_cancel; + plugin->proof + = &persona_proof; + plugin->proof_cancel + = &persona_proof_cancel; + plugin->webhook + = &persona_webhook; + plugin->webhook_cancel + = &persona_webhook_cancel; + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct TALER_KYCLOGIC_Plugin` + * @return NULL (always) + */ +void * +libtaler_plugin_kyclogic_persona_done (void *cls) +{ + struct TALER_KYCLOGIC_Plugin *plugin = cls; + struct PluginState *ps = plugin->cls; + + if (NULL != ps->curl_ctx) + { + GNUNET_CURL_fini (ps->curl_ctx); + ps->curl_ctx = NULL; + } + if (NULL != ps->curl_rc) + { + GNUNET_CURL_gnunet_rc_destroy (ps->curl_rc); + ps->curl_rc = NULL; + } + GNUNET_free (ps->exchange_base_url); + GNUNET_free (ps->webhook_token); + GNUNET_free (ps); + GNUNET_free (plugin); + return NULL; +} + + +/* end of plugin_kyclogic_persona.c */ diff --git a/src/kyclogic/plugin_kyclogic_template.c b/src/kyclogic/plugin_kyclogic_template.c new file mode 100644 index 000000000..54f36e6f2 --- /dev/null +++ b/src/kyclogic/plugin_kyclogic_template.c @@ -0,0 +1,468 @@ +/* + This file is part of GNU Taler + Copyright (C) 2022 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.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file plugin_kyclogic_template.c + * @brief template for an authentication flow logic + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_kyclogic_plugin.h" +#include "taler_mhd_lib.h" +#include "taler_json_lib.h" +#include <regex.h> +#include "taler_util.h" + + +/** + * Saves the state of a plugin. + */ +struct PluginState +{ + + /** + * Our base URL. + */ + char *exchange_base_url; + + /** + * Our global configuration. + */ + const struct GNUNET_CONFIGURATION_Handle *cfg; + + /** + * Context for CURL operations (useful to the event loop) + */ + struct GNUNET_CURL_Context *curl_ctx; + + /** + * Context for integrating @e curl_ctx with the + * GNUnet event loop. + */ + struct GNUNET_CURL_RescheduleContext *curl_rc; + +}; + + +/** + * Keeps the plugin-specific state for + * a given configuration section. + */ +struct TALER_KYCLOGIC_ProviderDetails +{ + + /** + * Overall plugin state. + */ + struct PluginState *ps; + + /** + * Configuration section that configured us. + */ + char *section; + +}; + + +/** + * Handle for an initiation operation. + */ +struct TALER_KYCLOGIC_InitiateHandle +{ + + /** + * Hash of the payto:// URI we are initiating + * the KYC for. + */ + struct TALER_PaytoHashP h_payto; + + /** + * UUID being checked. + */ + uint64_t legitimization_uuid; + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Continuation to call. + */ + TALER_KYCLOGIC_InitiateCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; +}; + + +/** + * Handle for an KYC proof operation. + */ +struct TALER_KYCLOGIC_ProofHandle +{ + + /** + * Overall plugin state. + */ + struct PluginState *ps; + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Continuation to call. + */ + TALER_KYCLOGIC_ProofCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; +}; + + +/** + * Handle for an KYC Web hook operation. + */ +struct TALER_KYCLOGIC_WebhookHandle +{ + + /** + * Continuation to call when done. + */ + TALER_KYCLOGIC_WebhookCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Task for asynchronous execution. + */ + struct GNUNET_SCHEDULER_Task *task; + + /** + * Overall plugin state. + */ + struct PluginState *ps; + + /** + * Our configuration details. + */ + const struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; +}; + + +/** + * Release configuration resources previously loaded + * + * @param[in] pd configuration to release + */ +static void +template_unload_configuration (struct TALER_KYCLOGIC_ProviderDetails *pd) +{ + GNUNET_free (pd); +} + + +/** + * Load the configuration of the KYC provider. + * + * @param cls closure + * @param provider_section_name configuration section to parse + * @return NULL if configuration is invalid + */ +static struct TALER_KYCLOGIC_ProviderDetails * +template_load_configuration (void *cls, + const char *provider_section_name) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_ProviderDetails *pd; + + pd = GNUNET_new (struct TALER_KYCLOGIC_ProviderDetails); + pd->ps = ps; + pd->section = GNUNET_strdup (provider_section_name); + GNUNET_break (0); // FIXME: parse config here! + return pd; +} + + +/** + * Cancel KYC check initiation. + * + * @param[in] ih handle of operation to cancel + */ +static void +template_initiate_cancel (struct TALER_KYCLOGIC_InitiateHandle *ih) +{ + GNUNET_break (0); // FIXME: add cancel logic here + GNUNET_free (ih); +} + + +/** + * Initiate KYC check. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param account_id which account to trigger process for + * @param legitimization_uuid unique ID for the legitimization process + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_InitiateHandle * +template_initiate (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + const struct TALER_PaytoHashP *account_id, + uint64_t legitimization_uuid, + TALER_KYCLOGIC_InitiateCallback cb, + void *cb_cls) +{ + struct TALER_KYCLOGIC_InitiateHandle *ih; + + (void) cls; + ih = GNUNET_new (struct TALER_KYCLOGIC_InitiateHandle); + ih->legitimization_uuid = legitimization_uuid; + ih->cb = cb; + ih->cb_cls = cb_cls; + ih->h_payto = *account_id; + ih->pd = pd; + GNUNET_break (0); // FIXME: add actual initiation logic! + return ih; +} + + +/** + * Cancel KYC proof. + * + * @param[in] ph handle of operation to cancel + */ +static void +template_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph) +{ + GNUNET_break (0); // FIXME: stop activities... + GNUNET_free (ph); +} + + +/** + * Check KYC status and return status to human. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param connection MHD connection object (for HTTP headers) + * @param account_id which account to trigger process for + * @param process_row row in the legitimization processes table the legitimization is for + * @param provider_user_id user ID (or NULL) the proof is for + * @param provider_legitimization_id legitimization ID the proof is for + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_ProofHandle * +template_proof (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + struct MHD_Connection *connection, + const struct TALER_PaytoHashP *account_id, + uint64_t process_row, + const char *provider_user_id, + const char *provider_legitimization_id, + TALER_KYCLOGIC_ProofCallback cb, + void *cb_cls) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_ProofHandle *ph; + + (void) account_id; + (void) process_row; + (void) provider_user_id; + (void) provider_legitimization_id; + ph = GNUNET_new (struct TALER_KYCLOGIC_ProofHandle); + ph->ps = ps; + ph->pd = pd; + ph->cb = cb; + ph->cb_cls = cb_cls; + ph->connection = connection; + + GNUNET_break (0); // FIXME: start check! + return ph; +} + + +/** + * Cancel KYC webhook execution. + * + * @param[in] wh handle of operation to cancel + */ +static void +template_webhook_cancel (struct TALER_KYCLOGIC_WebhookHandle *wh) +{ + GNUNET_break (0); /* FIXME: stop activity */ + GNUNET_free (wh); +} + + +/** + * Check KYC status and return result for Webhook. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param pd provider configuration details + * @param plc callback to lookup accounts with + * @param plc_cls closure for @a plc + * @param http_method HTTP method used for the webhook + * @param url_path rest of the URL after `/kyc-webhook/` + * @param connection MHD connection object (for HTTP headers) + * @param body HTTP request body + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel operation early + */ +static struct TALER_KYCLOGIC_WebhookHandle * +template_webhook (void *cls, + const struct TALER_KYCLOGIC_ProviderDetails *pd, + TALER_KYCLOGIC_ProviderLookupCallback plc, + void *plc_cls, + const char *http_method, + const char *const url_path[], + struct MHD_Connection *connection, + const json_t *body, + TALER_KYCLOGIC_WebhookCallback cb, + void *cb_cls) +{ + struct PluginState *ps = cls; + struct TALER_KYCLOGIC_WebhookHandle *wh; + + (void) plc; + (void) plc_cls; + (void) http_method; + (void) url_path; + (void) body; + wh = GNUNET_new (struct TALER_KYCLOGIC_WebhookHandle); + wh->cb = cb; + wh->cb_cls = cb_cls; + wh->ps = ps; + wh->pd = pd; + wh->connection = connection; + GNUNET_break (0); /* FIXME: start activity */ + return wh; +} + + +/** + * Initialize Template.0 KYC logic plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct TALER_KYCLOGIC_Plugin` + */ +void * +libtaler_plugin_kyclogic_template_init (void *cls) +{ + const struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct TALER_KYCLOGIC_Plugin *plugin; + struct PluginState *ps; + + ps = GNUNET_new (struct PluginState); + ps->cfg = cfg; + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "exchange", + "BASE_URL", + &ps->exchange_base_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "BASE_URL"); + GNUNET_free (ps); + return NULL; + } + + ps->curl_ctx + = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &ps->curl_rc); + if (NULL == ps->curl_ctx) + { + GNUNET_break (0); + GNUNET_free (ps->exchange_base_url); + GNUNET_free (ps); + return NULL; + } + ps->curl_rc = GNUNET_CURL_gnunet_rc_create (ps->curl_ctx); + + plugin = GNUNET_new (struct TALER_KYCLOGIC_Plugin); + plugin->cls = ps; + plugin->load_configuration + = &template_load_configuration; + plugin->unload_configuration + = &template_unload_configuration; + plugin->initiate + = &template_initiate; + plugin->initiate_cancel + = &template_initiate_cancel; + plugin->proof + = &template_proof; + plugin->proof_cancel + = &template_proof_cancel; + plugin->webhook + = &template_webhook; + plugin->webhook_cancel + = &template_webhook_cancel; + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct TALER_KYCLOGIC_Plugin` + * @return NULL (always) + */ +void * +libtaler_plugin_kyclogic_template_done (void *cls) +{ + struct TALER_KYCLOGIC_Plugin *plugin = cls; + struct PluginState *ps = plugin->cls; + + if (NULL != ps->curl_ctx) + { + GNUNET_CURL_fini (ps->curl_ctx); + ps->curl_ctx = NULL; + } + if (NULL != ps->curl_rc) + { + GNUNET_CURL_gnunet_rc_destroy (ps->curl_rc); + ps->curl_rc = NULL; + } + GNUNET_free (ps->exchange_base_url); + GNUNET_free (ps); + GNUNET_free (plugin); + return NULL; +} diff --git a/src/kyclogic/sample.conf b/src/kyclogic/sample.conf new file mode 100644 index 000000000..b9a88c292 --- /dev/null +++ b/src/kyclogic/sample.conf @@ -0,0 +1,33 @@ +# This file is in the public domain. +# + +[exchange] + +# HTTP port the exchange listens to +PORT = 8081 + +# Base URL of the exchange. Must be set to a URL where the +# exchange (or the twister) is actually listening. +BASE_URL = "http://localhost:8081/" + +[kyc-provider-test-oauth2] + +COST = 0 +LOGIC = oauth2 +USER_TYPE = INDIVIDUAL +PROVIDED_CHECKS = DUMMY + +KYC_OAUTH2_VALIDITY = forever +KYC_OAUTH2_AUTH_URL = http://kyc.taler.net/auth +KYC_OAUTH2_LOGIN_URL = http://kyc.taler.net/login +KYC_OAUTH2_INFO_URL = http://kyc.taler.net/info +KYC_OAUTH2_POST_URL = http://kyc.taler.net/thank-you +KYC_OAUTH2_CLIENT_ID = testcase +KYC_OAUTH2_CLIENT_SECRET = password + +[kyc-legitimization-withdraw-high] + +OPERATION_TYPE = WITHDRAW +REQUIRED_CHECKS = DUMMY +THRESHOLD = KUDOS:100 +TIMEFRAME = 1a diff --git a/src/kyclogic/taler-exchange-kyc-kycaid-converter.sh b/src/kyclogic/taler-exchange-kyc-kycaid-converter.sh new file mode 100755 index 000000000..68a1b6a0d --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-kycaid-converter.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# This file is in the public domain. +# +# This code converts (some of) the JSON output from KYCAID into the GNU Taler +# specific KYC attribute data (again in JSON format). We may need to download +# and inline file data in the process, for authorization pass "-a" with the +# respective bearer token. +# + +# Die if anything goes wrong. +set -eu + +# Parse command-line options +while getopts ':a:' OPTION; do + case "$OPTION" in + a) + TOKEN="$OPTARG" + ;; + ?) + echo "Unrecognized command line option" + exit 1 + ;; + esac +done + +# First, extract everything from stdin. +J=$(jq '{"type":.type,"email":.email,"phone":.phone,"first_name":.first_name,"name-middle":.middle_name,"last_name":.last_name,"dob":.dob,"residence_country":.residence_country,"gender":.gender,"pep":.pep,"addresses":.addresses,"documents":.documents,"company_name":.company_name,"business_activity_id":.business_activity_id,"registration_country":.registration_country,"documents":.documents,"decline_reasons":.decline_reasons}') + +# TODO: +# log_failure (json_object_get (j, "decline_reasons")); + +TYPE=$(echo "$J" | jq -r '.type') + +N=0 +DOCS_RAW="" +DOCS_JSON="" +for ID in $(jq -r '.documents[]|select(.status=="valid")|.id') +do + TYPE=$(jq -r ".documents[]|select(.id==\"$ID\")|.type") + EXPIRY=$(jq -r ".documents[]|select(.id==\"$ID\")|.expiry_date") + DOCUMENT_FILE=$(mktemp -t tmp.XXXXXXXXXX) + # Authorization: Token $TOKEN + DOCUMENT_URL="https://api.kycaid.com/documents/$ID" + if [ -z "${TOKEN:-}" ] + then + wget -q --output-document=- "$DOCUMENT_URL" \ + | gnunet-base32 > ${DOCUMENT_FILE} + else + wget -q --output-document=- "$DOCUMENT_URL" \ + --header "Authorization: Token $TOKEN" \ + | gnunet-base32 > ${DOCUMENT_FILE} + fi + DOCS_RAW="$DOCS_RAW --rawfile photo$N \"${DOCUMENT_FILE}\"" + if [ "$N" = 0 ] + then + DOCS_JSON="{\"type\":\"$TYPE\",\"image\":\$photo$N}" + else + DOCS_JSON="{\"type\":\"$TYPE\",\"image\":\$photo$N},$DOCS_JSON" + fi + N=$(expr $N + 1) +done + + +if [ "PERSON" = "${TYPE}" ] +then + + # Next, combine some fields into larger values. + FULLNAME=$(echo "$J" | jq -r '[.first_name,.middle_name,.last_name]|join(" ")') +# STREET=$(echo $J | jq -r '[."street-1",."street-2"]|join(" ")') +# CITY=$(echo $J | jq -r '[.postcode,.city,."address-subdivision,.cc"]|join(" ")') + + # Combine into final result for individual. + echo "$J" \ + | jq \ + --arg full_name "${FULLNAME}" \ + '{$full_name,"birthdate":.dob,"pep":.pep,"phone":.phone,"email":.email,"residences":.residence_country}' \ + | jq \ + 'del(..|select(.==null))' + +else + # Combine into final result for business. + echo "$J" \ + | jq \ + $DOCS_RAW \ + "{\"company_name\":.company_name,\"phone\":.phone,\"email\":.email,\"registration_country\":.registration_country,\"documents\":[${DOCS_JSON}]}" \ + | jq \ + 'del(..|select(.==null))' +fi + +exit 0 diff --git a/src/kyclogic/taler-exchange-kyc-oauth2-challenger.sh b/src/kyclogic/taler-exchange-kyc-oauth2-challenger.sh new file mode 100755 index 000000000..729abc504 --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-oauth2-challenger.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# This file is in the public domain. +# +# This code converts (some of) the JSON output from +# Challenger into the GNU Taler +# specific KYC attribute data (again in JSON format). +# + +# Die if anything goes wrong. +set -eu + +# First, extract everything from stdin. +J=$(jq '{"id":.id,"email":.address,"type":.address_type,"expires":.address_expiration}') + +ADDRESS_TYPE=$(echo "$J" | jq -r '.type') +ROWID=$(echo "$J" | jq -r '.id') +if [ "$ADDRESS_TYPE" != "email" ] +then + return 1 +fi + +echo "$J" \ + | jq \ + --arg id "${ROWID}" \ + '{$id,"email":.email,"expires":.expires}' + +exit 0 diff --git a/src/kyclogic/taler-exchange-kyc-oauth2-nda.sh b/src/kyclogic/taler-exchange-kyc-oauth2-nda.sh new file mode 100755 index 000000000..5af785f19 --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-oauth2-nda.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# This file is in the public domain. +# +# This code converts (some of) the JSON output from NDA into the GNU Taler +# specific KYC attribute data (again in JSON format). +# + +# Die if anything goes wrong. +set -eu + +# First, extract everything from stdin. +J=$(jq '{"status":.status,"id":.data.id,"last":.data.last_name,"first":.data.first_name,"phone":.data.phone}') + +STATUS=$(echo "$J" | jq -r '.status') +if [ "$STATUS" != "success" ] +then + return 1 +fi + +# Next, combine some fields into larger values. +FULLNAME=$(echo "$J" | jq -r '[.first_name,.last_name]|join(" ")') + +echo "$J" \ + | jq \ + --arg full_name "${FULLNAME}" \ + '{$full_name,"phone":.phone,"id":.id}' \ + | jq \ + 'del(..|select(.==null))' + +exit 0 diff --git a/src/kyclogic/taler-exchange-kyc-oauth2-test-converter.sh b/src/kyclogic/taler-exchange-kyc-oauth2-test-converter.sh new file mode 100755 index 000000000..76f9f16c4 --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-oauth2-test-converter.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# This file is in the public domain. +# +# This code converts (some of) the JSON output from +# Challenger into the GNU Taler +# specific KYC attribute data (again in JSON format). +# + +# Die if anything goes wrong. +set -eu + + +# First, extract everything from stdin. +J=$(jq '{"id":.data.id,"first":.data.first_name,"last":.data.last_name,"birthdate":.data.birthdate,"status":.status}') + +# Next, combine some fields into larger values. +STATUS=$(echo "$J" | jq -r '.status') +if [ "$STATUS" != "success" ] +then + exit 1 +fi + +FULLNAME=$(echo "$J" | jq -r '[.first,.last]|join(" ")') + +echo $J \ + | jq \ + --arg full_name "${FULLNAME}" \ + '{$full_name,"birthdate":.birthdate,"id":.id}' \ + | jq \ + 'del(..|select(.==null))' +exit 0 diff --git a/src/kyclogic/taler-exchange-kyc-persona-converter.sh b/src/kyclogic/taler-exchange-kyc-persona-converter.sh new file mode 100755 index 000000000..13142d0e5 --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-persona-converter.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# This file is in the public domain. +# +# This code converts (some of) the JSON output from Persona into the GNU Taler +# specific KYC attribute data (again in JSON format). We may need to download +# and inline file data in the process, for authorization pass "-a" with the +# respective bearer token. +# + +# Die if anything goes wrong. +set -eu + +# Parse command-line options +while getopts ':a:' OPTION; do + case "$OPTION" in + a) + TOKEN="$OPTARG" + ;; + ?) + echo "Unrecognized command line option" + exit 1 + ;; + esac +done + + +# First, extract everything from stdin. +J=$(jq '{"first":.data.attributes."name-first","middle":.data.attributes."name-middle","last":.data.attributes."name-last","cc":.data.attributes.fields."address-country-code".value,"birthdate":.data.attributes.birthdate,"city":.data.attributes."address-city","postcode":.data.attributes."address-postal-code","street-1":.data.attributes."address-street-1","street-2":.data.attributes."address-street-2","address-subdivision":.data.attributes."address-subdivision","identification-number":.data.attributes."identification-number","photo":.included[]|select(.type=="verification/government-id")|.attributes|select(.status=="passed")|."front-photo-url"}') + + +# Next, combine some fields into larger values. +FULLNAME=$(echo "$J" | jq -r '[.first,.middle,.last]|join(" ")') +STREET=$(echo $J | jq -r '[."street-1",."street-2"]|join(" ")') +CITY=$(echo $J | jq -r '[.postcode,.city,."address-subdivision,.cc"]|join(" ")') + +# Download and base32-encode the photo +PHOTO_URL=$(echo "$J" | jq -r '.photo') +PHOTO_FILE=$(mktemp -t tmp.XXXXXXXXXX) +if [ -z "${TOKEN:-}" ] +then + wget -q --output-document=- "$PHOTO_URL" | gnunet-base32 > ${PHOTO_FILE} +else + wget -q --output-document=- --header "Authorization: Bearer $TOKEN" "$PHOTO_URL" | gnunet-base32 > ${PHOTO_FILE} +fi + +# Combine into final result. +echo "$J" \ + | jq \ + --arg full_name "${FULLNAME}" \ + --arg street "${STREET}" \ + --arg city "${CITY}" \ + --rawfile photo "${PHOTO_FILE}" \ + '{$full_name,$street,$city,"birthdate":.birthdate,"residences":.cc,"identification_number":."identification-number",$photo}' \ + | jq \ + 'del(..|select(.==null))' + +exit 0 diff --git a/src/kyclogic/taler-exchange-kyc-tester.c b/src/kyclogic/taler-exchange-kyc-tester.c new file mode 100644 index 000000000..c2efafd72 --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-tester.c @@ -0,0 +1,1646 @@ +/* + This file is part of TALER + Copyright (C) 2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file taler-exchange-kyc-tester.c + * @brief tool to test KYC integrations + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <sched.h> +#include <sys/resource.h> +#include <limits.h> +#include "taler_mhd_lib.h" +#include "taler_json_lib.h" +#include "taler_templating_lib.h" +#include "taler_util.h" +#include "taler_kyclogic_lib.h" +#include "taler_kyclogic_plugin.h" +#include <gnunet/gnunet_mhd_compat.h> + + +/** + * @brief Context in which the exchange is processing + * all requests + */ +struct TEKT_RequestContext +{ + + /** + * Opaque parsing context. + */ + void *opaque_post_parsing_context; + + /** + * Request handler responsible for this request. + */ + const struct TEKT_RequestHandler *rh; + + /** + * Request URL (for logging). + */ + const char *url; + + /** + * Connection we are processing. + */ + struct MHD_Connection *connection; + + /** + * HTTP response to return (or NULL). + */ + struct MHD_Response *response; + + /** + * @e rh-specific cleanup routine. Function called + * upon completion of the request that should + * clean up @a rh_ctx. Can be NULL. + */ + void + (*rh_cleaner)(struct TEKT_RequestContext *rc); + + /** + * @e rh-specific context. Place where the request + * handler can associate state with this request. + * Can be NULL. + */ + void *rh_ctx; + + /** + * Uploaded JSON body, if any. + */ + json_t *root; + + /** + * HTTP status to return upon resume if @e response + * is non-NULL. + */ + unsigned int http_status; + +}; + + +/** + * @brief Struct describing an URL and the handler for it. + */ +struct TEKT_RequestHandler +{ + + /** + * URL the handler is for (first part only). + */ + const char *url; + + /** + * Method the handler is for. + */ + const char *method; + + /** + * Callbacks for handling of the request. Which one is used + * depends on @e method. + */ + union + { + /** + * Function to call to handle a GET requests (and those + * with @e method NULL). + * + * @param rc context for the request + * @param mime_type the @e mime_type for the reply (hint, can be NULL) + * @param args array of arguments, needs to be of length @e args_expected + * @return MHD result code + */ + MHD_RESULT + (*get)(struct TEKT_RequestContext *rc, + const char *const args[]); + + + /** + * Function to call to handle a POST request. + * + * @param rc context for the request + * @param json uploaded JSON data + * @param args array of arguments, needs to be of length @e args_expected + * @return MHD result code + */ + MHD_RESULT + (*post)(struct TEKT_RequestContext *rc, + const json_t *root, + const char *const args[]); + + } handler; + + /** + * Number of arguments this handler expects in the @a args array. + */ + unsigned int nargs; + + /** + * Is the number of arguments given in @e nargs only an upper bound, + * and calling with fewer arguments could be OK? + */ + bool nargs_is_upper_bound; + + /** + * Mime type to use in reply (hint, can be NULL). + */ + const char *mime_type; + + /** + * Raw data for the @e handler, can be NULL for none provided. + */ + const void *data; + + /** + * Number of bytes in @e data, 0 for data is 0-terminated (!). + */ + size_t data_size; + + /** + * Default response code. 0 for none provided. + */ + unsigned int response_code; +}; + + +/** + * Information we track per ongoing kyc-proof request. + */ +struct ProofRequestState +{ + /** + * Kept in a DLL. + */ + struct ProofRequestState *next; + + /** + * Kept in a DLL. + */ + struct ProofRequestState *prev; + + /** + * Handle for operation with the plugin. + */ + struct TALER_KYCLOGIC_ProofHandle *ph; + + /** + * Logic plugin we are using. + */ + struct TALER_KYCLOGIC_Plugin *logic; + + /** + * HTTP request details. + */ + struct TEKT_RequestContext *rc; + +}; + +/** + * Head of DLL. + */ +static struct ProofRequestState *rs_head; + +/** + * Tail of DLL. + */ +static struct ProofRequestState *rs_tail; + +/** + * The exchange's configuration (global) + */ +static const struct GNUNET_CONFIGURATION_Handle *TEKT_cfg; + +/** + * Handle to the HTTP server. + */ +static struct MHD_Daemon *mhd; + +/** + * Our base URL. + */ +static char *TEKT_base_url; + +/** + * Payto set via command-line (or otherwise random). + */ +static struct TALER_PaytoHashP cmd_line_h_payto; + +/** + * Provider user ID to use. + */ +static char *cmd_provider_user_id; + +/** + * Provider legitimization ID to use. + */ +static char *cmd_provider_legitimization_id; + +/** + * Name of the configuration section with the + * configuration data of the selected provider. + */ +static const char *provider_section_name; + +/** + * Row ID to use, override with '-r' + */ +static unsigned int kyc_row_id = 42; + +/** + * -P command-line option. + */ +static int print_h_payto; + +/** + * -w command-line option. + */ +static int run_webservice; + +/** + * Value to return from main() + */ +static int global_ret; + +/** + * -r command-line flag. + */ +static char *requirements; + +/** + * -i command-line flag. + */ +static char *ut_s = "individual"; + +/** + * Handle for ongoing initiation operation. + */ +static struct TALER_KYCLOGIC_InitiateHandle *ih; + +/** + * KYC logic running for @e ih. + */ +static struct TALER_KYCLOGIC_Plugin *ih_logic; + +/** + * Port to run the daemon on. + */ +static uint16_t serve_port; + +/** + * Context for all CURL operations (useful to the event loop) + */ +static struct GNUNET_CURL_Context *TEKT_curl_ctx; + +/** + * Context for integrating #TEKT_curl_ctx with the + * GNUnet event loop. + */ +static struct GNUNET_CURL_RescheduleContext *exchange_curl_rc; + + +/** + * Context for the webhook. + */ +struct KycWebhookContext +{ + + /** + * Kept in a DLL while suspended. + */ + struct KycWebhookContext *next; + + /** + * Kept in a DLL while suspended. + */ + struct KycWebhookContext *prev; + + /** + * Details about the connection we are processing. + */ + struct TEKT_RequestContext *rc; + + /** + * Plugin responsible for the webhook. + */ + struct TALER_KYCLOGIC_Plugin *plugin; + + /** + * Configuration for the specific action. + */ + struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Webhook activity. + */ + struct TALER_KYCLOGIC_WebhookHandle *wh; + + /** + * HTTP response to return. + */ + struct MHD_Response *response; + + /** + * Name of the configuration + * section defining the KYC logic. + */ + const char *section_name; + + /** + * HTTP response code to return. + */ + unsigned int response_code; + + /** + * #GNUNET_YES if we are suspended, + * #GNUNET_NO if not. + * #GNUNET_SYSERR if we had some error. + */ + enum GNUNET_GenericReturnValue suspended; + +}; + + +/** + * Contexts are kept in a DLL while suspended. + */ +static struct KycWebhookContext *kwh_head; + +/** + * Contexts are kept in a DLL while suspended. + */ +static struct KycWebhookContext *kwh_tail; + + +/** + * Resume processing the @a kwh request. + * + * @param kwh request to resume + */ +static void +kwh_resume (struct KycWebhookContext *kwh) +{ + GNUNET_assert (GNUNET_YES == kwh->suspended); + kwh->suspended = GNUNET_NO; + GNUNET_CONTAINER_DLL_remove (kwh_head, + kwh_tail, + kwh); + MHD_resume_connection (kwh->rc->connection); +} + + +static void +kyc_webhook_cleanup (void) +{ + struct KycWebhookContext *kwh; + + while (NULL != (kwh = kwh_head)) + { + if (NULL != kwh->wh) + { + kwh->plugin->webhook_cancel (kwh->wh); + kwh->wh = NULL; + } + kwh_resume (kwh); + } +} + + +/** + * Function called with the result of a webhook + * operation. + * + * Note that the "decref" for the @a response + * will be done by the plugin. + * + * @param cls closure + * @param process_row legitimization process request the webhook was about + * @param account_id account the webhook was about + * @param provider_section configuration section of the logic + * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + * @param status KYC status + * @param expiration until when is the KYC check valid + * @param attributes user attributes returned by the provider + * @param http_status HTTP status code of @a response + * @param[in] response to return to the HTTP client + */ +static void +webhook_finished_cb ( + void *cls, + uint64_t process_row, + const struct TALER_PaytoHashP *account_id, + const char *provider_section, + const char *provider_user_id, + const char *provider_legitimization_id, + enum TALER_KYCLOGIC_KycStatus status, + struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, + unsigned int http_status, + struct MHD_Response *response) +{ + struct KycWebhookContext *kwh = cls; + + (void) expiration; + (void) provider_section; + kwh->wh = NULL; + if ( (NULL != account_id) && + (0 != GNUNET_memcmp (account_id, + &cmd_line_h_payto)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received webhook for unexpected account\n"); + } + if ( (NULL != provider_user_id) && + (NULL != cmd_provider_user_id) && + (0 != strcmp (provider_user_id, + cmd_provider_user_id)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received webhook for unexpected provider user ID (%s)\n", + provider_user_id); + } + if ( (NULL != provider_legitimization_id) && + (NULL != cmd_provider_legitimization_id) && + (0 != strcmp (provider_legitimization_id, + cmd_provider_legitimization_id)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received webhook for unexpected provider legitimization ID (%s)\n", + provider_legitimization_id); + } + switch (status) + { + case TALER_KYCLOGIC_STATUS_SUCCESS: + /* _successfully_ resumed case */ + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "KYC successful for user `%s' (legi: %s)\n", + provider_user_id, + provider_legitimization_id); + GNUNET_break (NULL != attributes); + fprintf (stderr, + "Extracted attributes:\n"); + json_dumpf (attributes, + stderr, + JSON_INDENT (2)); + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC status of %s/%s (process #%llu) is %d\n", + provider_user_id, + provider_legitimization_id, + (unsigned long long) process_row, + status); + break; + } + kwh->response = response; + kwh->response_code = http_status; + kwh_resume (kwh); + TALER_MHD_daemon_trigger (); +} + + +/** + * Function called to clean up a context. + * + * @param rc request context + */ +static void +clean_kwh (struct TEKT_RequestContext *rc) +{ + struct KycWebhookContext *kwh = rc->rh_ctx; + + if (NULL != kwh->wh) + { + kwh->plugin->webhook_cancel (kwh->wh); + kwh->wh = NULL; + } + if (NULL != kwh->response) + { + MHD_destroy_response (kwh->response); + kwh->response = NULL; + } + GNUNET_free (kwh); +} + + +/** + * Function the plugin can use to lookup an + * @a h_payto by @a provider_legitimization_id. + * + * @param cls closure, NULL + * @param provider_section + * @param provider_legitimization_id legi to look up + * @param[out] h_payto where to write the result + * @param[out] legi_row where to write the row ID for the legitimization ID + * @return database transaction status + */ +static enum GNUNET_DB_QueryStatus +kyc_provider_account_lookup ( + void *cls, + const char *provider_section, + const char *provider_legitimization_id, + struct TALER_PaytoHashP *h_payto, + uint64_t *legi_row) +{ + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Simulated account lookup using `%s/%s'\n", + provider_section, + provider_legitimization_id); + *h_payto = cmd_line_h_payto; + *legi_row = kyc_row_id; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +/** + * Handle a (GET or POST) "/kyc-webhook" request. + * + * @param rc request to handle + * @param method HTTP request method used by the client + * @param root uploaded JSON body (can be NULL) + * @param args one argument with the legitimization_uuid + * @return MHD result code + */ +static MHD_RESULT +handler_kyc_webhook_generic ( + struct TEKT_RequestContext *rc, + const char *method, + const json_t *root, + const char *const args[]) +{ + struct KycWebhookContext *kwh = rc->rh_ctx; + + if (NULL == kwh) + { /* first time */ + kwh = GNUNET_new (struct KycWebhookContext); + kwh->rc = rc; + rc->rh_ctx = kwh; + rc->rh_cleaner = &clean_kwh; + + if ( (NULL == args[0]) || + (GNUNET_OK != + TALER_KYCLOGIC_lookup_logic (args[0], + &kwh->plugin, + &kwh->pd, + &kwh->section_name)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYC logic `%s' unknown (check KYC provider configuration)\n", + args[0]); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, + args[0]); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Calling KYC provider specific webhook\n"); + kwh->wh = kwh->plugin->webhook (kwh->plugin->cls, + kwh->pd, + &kyc_provider_account_lookup, + NULL, + method, + &args[1], + rc->connection, + root, + &webhook_finished_cb, + kwh); + if (NULL == kwh->wh) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "failed to run webhook logic"); + } + kwh->suspended = GNUNET_YES; + GNUNET_CONTAINER_DLL_insert (kwh_head, + kwh_tail, + kwh); + MHD_suspend_connection (rc->connection); + return MHD_YES; + } + + if (NULL != kwh->response) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning queued reply for KWH\n"); + /* handle _failed_ resumed cases */ + return MHD_queue_response (rc->connection, + kwh->response_code, + kwh->response); + } + + /* We resumed, but got no response? This should + not happen. */ + GNUNET_assert (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "resumed without response"); +} + + +/** + * Handle a GET "/kyc-webhook" request. + * + * @param rc request to handle + * @param args one argument with the legitimization_uuid + * @return MHD result code + */ +static MHD_RESULT +handler_kyc_webhook_get ( + struct TEKT_RequestContext *rc, + const char *const args[]) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Webhook GET triggered\n"); + return handler_kyc_webhook_generic (rc, + MHD_HTTP_METHOD_GET, + NULL, + args); +} + + +/** + * Handle a POST "/kyc-webhook" request. + * + * @param rc request to handle + * @param root uploaded JSON body (can be NULL) + * @param args one argument with the legitimization_uuid + * @return MHD result code + */ +static MHD_RESULT +handler_kyc_webhook_post ( + struct TEKT_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Webhook POST triggered\n"); + return handler_kyc_webhook_generic (rc, + MHD_HTTP_METHOD_POST, + root, + args); +} + + +/** + * Function called with the result of a proof check operation. + * + * Note that the "decref" for the @a response + * will be done by the callee and MUST NOT be done by the plugin. + * + * @param cls closure with the `struct ProofRequestState` + * @param status KYC status + * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + * @param expiration until when is the KYC check valid + * @param attributes attributes about the user + * @param http_status HTTP status code of @a response + * @param[in] response to return to the HTTP client + */ +static void +proof_cb ( + void *cls, + enum TALER_KYCLOGIC_KycStatus status, + const char *provider_user_id, + const char *provider_legitimization_id, + struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, + unsigned int http_status, + struct MHD_Response *response) +{ + struct ProofRequestState *rs = cls; + + (void) expiration; + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "KYC legitimization %s completed with status %d (%u) for %s\n", + provider_legitimization_id, + status, + http_status, + provider_user_id); + if (TALER_KYCLOGIC_STATUS_SUCCESS == status) + { + GNUNET_break (NULL != attributes); + fprintf (stderr, + "Extracted attributes:\n"); + json_dumpf (attributes, + stderr, + JSON_INDENT (2)); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning response %p with status %u\n", + response, + http_status); + rs->rc->response = response; + rs->rc->http_status = http_status; + GNUNET_CONTAINER_DLL_remove (rs_head, + rs_tail, + rs); + MHD_resume_connection (rs->rc->connection); + TALER_MHD_daemon_trigger (); + GNUNET_free (rs); +} + + +/** + * Function called when we receive a 'GET' to the + * '/kyc-proof' endpoint. + * + * @param rc request context + * @param args remaining URL arguments; + * args[0] should be the logic plugin name + */ +static MHD_RESULT +handler_kyc_proof_get ( + struct TEKT_RequestContext *rc, + const char *const args[1]) +{ + struct TALER_PaytoHashP h_payto; + struct TALER_KYCLOGIC_ProviderDetails *pd; + struct TALER_KYCLOGIC_Plugin *logic; + struct ProofRequestState *rs; + const char *section_name; + const char *h_paytos; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "GET /kyc-proof triggered\n"); + if (NULL == args[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + "'/kyc-proof/$PROVIDER_SECTION?state=$H_PAYTO' required"); + } + h_paytos = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "state"); + if (NULL == h_paytos) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "h_payto"); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (h_paytos, + strlen (h_paytos), + &h_payto, + sizeof (h_payto))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "h_payto"); + } + if (0 != + GNUNET_memcmp (&h_payto, + &cmd_line_h_payto)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, + "h_payto"); + } + + if (GNUNET_OK != + TALER_KYCLOGIC_lookup_logic (args[0], + &logic, + &pd, + §ion_name)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not initiate KYC with provider `%s' (configuration error?)\n", + args[0]); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, + args[0]); + } + rs = GNUNET_new (struct ProofRequestState); + rs->rc = rc; + rs->logic = logic; + MHD_suspend_connection (rc->connection); + GNUNET_CONTAINER_DLL_insert (rs_head, + rs_tail, + rs); + rs->ph = logic->proof (logic->cls, + pd, + rc->connection, + &h_payto, + kyc_row_id, + cmd_provider_user_id, + cmd_provider_legitimization_id, + &proof_cb, + rs); + GNUNET_assert (NULL != rs->ph); + return MHD_YES; +} + + +/** + * 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 TEKT_RequestContext *rc = *con_cls; + + (void) cls; + if (NULL == rc) + return; + if (NULL != rc->rh_cleaner) + rc->rh_cleaner (rc); + { +#if MHD_VERSION >= 0x00097304 + const union MHD_ConnectionInfo *ci; + unsigned int http_status = 0; + + ci = MHD_get_connection_info (connection, + MHD_CONNECTION_INFO_HTTP_STATUS); + if (NULL != ci) + http_status = ci->http_status; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request for `%s' completed with HTTP status %u (%d)\n", + rc->url, + http_status, + toe); +#else + (void) connection; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request for `%s' completed (%d)\n", + rc->url, + toe); +#endif + } + + TALER_MHD_parse_post_cleanup_callback (rc->opaque_post_parsing_context); + /* Sanity-check that we didn't leave any transactions hanging */ + if (NULL != rc->root) + json_decref (rc->root); + GNUNET_free (rc); + *con_cls = NULL; +} + + +/** + * We found a request handler responsible for handling a request. Parse the + * @a upload_data (if applicable) and the @a url and call the + * handler. + * + * @param rc request context + * @param url rest of the URL to parse + * @param upload_data upload data to parse (if available) + * @param[in,out] upload_data_size number of bytes in @a upload_data + * @return MHD result code + */ +static MHD_RESULT +proceed_with_handler (struct TEKT_RequestContext *rc, + const char *url, + const char *upload_data, + size_t *upload_data_size) +{ + const struct TEKT_RequestHandler *rh = rc->rh; + const char *args[rh->nargs + 2]; + size_t ulen = strlen (url) + 1; + MHD_RESULT ret; + + /* We do check for "ulen" here, because we'll later stack-allocate a buffer + of that size and don't want to enable malicious clients to cause us + huge stack allocations. */ + if (ulen > 512) + { + /* 512 is simply "big enough", as it is bigger than "6 * 54", + which is the longest URL format we ever get (for + /deposits/). The value should be adjusted if we ever define protocol + endpoints with plausibly longer inputs. */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_URI_TOO_LONG, + TALER_EC_GENERIC_URI_TOO_LONG, + url); + } + + /* All POST endpoints come with a body in JSON format. So we parse + the JSON here. */ + if ( (NULL == rc->root) && + (0 == strcasecmp (rh->method, + MHD_HTTP_METHOD_POST)) ) + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_post_json (rc->connection, + &rc->opaque_post_parsing_context, + upload_data, + upload_data_size, + &rc->root); + if (GNUNET_SYSERR == res) + { + GNUNET_assert (NULL == rc->root); + GNUNET_break (0); + return MHD_NO; /* bad upload, could not even generate error */ + } + if ( (GNUNET_NO == res) || + (NULL == rc->root) ) + { + GNUNET_assert (NULL == rc->root); + return MHD_YES; /* so far incomplete upload or parser error */ + } + } + + { + char d[ulen]; + unsigned int i; + char *sp; + + /* Parse command-line arguments */ + /* make a copy of 'url' because 'strtok_r()' will modify */ + GNUNET_memcpy (d, + url, + ulen); + i = 0; + args[i++] = strtok_r (d, "/", &sp); + while ( (NULL != args[i - 1]) && + (i <= rh->nargs + 1) ) + args[i++] = strtok_r (NULL, "/", &sp); + /* make sure above loop ran nicely until completion, and also + that there is no excess data in 'd' afterwards */ + if ( ( (rh->nargs_is_upper_bound) && + (i - 1 > rh->nargs) ) || + ( (! rh->nargs_is_upper_bound) && + (i - 1 != rh->nargs) ) ) + { + char emsg[128 + 512]; + + GNUNET_snprintf (emsg, + sizeof (emsg), + "Got %u+/%u segments for `%s' request (`%s')", + i - 1, + rh->nargs, + rh->url, + url); + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS, + emsg); + } + GNUNET_assert (NULL == args[i - 1]); + + /* Above logic ensures that 'root' is exactly non-NULL for POST operations, + so we test for 'root' to decide which handler to invoke. */ + if (NULL != rc->root) + ret = rh->handler.post (rc, + rc->root, + args); + else /* We also only have "POST" or "GET" in the API for at this point + (OPTIONS/HEAD are taken care of earlier) */ + ret = rh->handler.get (rc, + args); + } + return ret; +} + + +static void +rh_cleaner_cb (struct TEKT_RequestContext *rc) +{ + if (NULL != rc->response) + { + MHD_destroy_response (rc->response); + rc->response = NULL; + } + if (NULL != rc->root) + { + json_decref (rc->root); + rc->root = NULL; + } +} + + +/** + * Handle incoming HTTP request. + * + * @param cls closure for MHD daemon (unused) + * @param connection the connection + * @param url the requested url + * @param method the method (POST, GET, ...) + * @param version HTTP version (ignored) + * @param upload_data request data + * @param upload_data_size size of @a upload_data in bytes + * @param con_cls closure for request (a `struct TEKT_RequestContext *`) + * @return MHD result code + */ +static MHD_RESULT +handle_mhd_request (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 TEKT_RequestHandler handlers[] = { + /* simulated KYC endpoints */ + { + .url = "kyc-proof", + .method = MHD_HTTP_METHOD_GET, + .handler.get = &handler_kyc_proof_get, + .nargs = 1 + }, + { + .url = "kyc-webhook", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &handler_kyc_webhook_post, + .nargs = 128, + .nargs_is_upper_bound = true + }, + { + .url = "kyc-webhook", + .method = MHD_HTTP_METHOD_GET, + .handler.get = &handler_kyc_webhook_get, + .nargs = 128, + .nargs_is_upper_bound = true + }, + /* mark end of list */ + { + .url = NULL + } + }; + struct TEKT_RequestContext *rc = *con_cls; + + (void) cls; + (void) version; + if (NULL == rc) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling new request\n"); + /* We're in a new async scope! */ + rc = *con_cls = GNUNET_new (struct TEKT_RequestContext); + rc->url = url; + rc->connection = connection; + rc->rh_cleaner = &rh_cleaner_cb; + } + if (NULL != rc->response) + { + return MHD_queue_response (rc->connection, + rc->http_status, + rc->response); + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling request (%s) for URL '%s'\n", + method, + url); + /* on repeated requests, check our cache first */ + if (NULL != rc->rh) + { + const char *start; + + if ('\0' == url[0]) + /* strange, should start with '/', treat as just "/" */ + url = "/"; + start = strchr (url + 1, '/'); + if (NULL == start) + start = ""; + return proceed_with_handler (rc, + start, + upload_data, + upload_data_size); + } + if (0 == strcasecmp (method, + MHD_HTTP_METHOD_HEAD)) + method = MHD_HTTP_METHOD_GET; /* treat HEAD as GET here, MHD will do the rest */ + + /* parse first part of URL */ + { + bool found = false; + size_t tok_size; + const char *tok; + const char *rest; + + if ('\0' == url[0]) + /* strange, should start with '/', treat as just "/" */ + url = "/"; + tok = url + 1; + rest = strchr (tok, '/'); + if (NULL == rest) + { + tok_size = strlen (tok); + } + else + { + tok_size = rest - tok; + rest++; /* skip over '/' */ + } + for (unsigned int i = 0; NULL != handlers[i].url; i++) + { + struct TEKT_RequestHandler *rh = &handlers[i]; + + if ( (0 != strncmp (tok, + rh->url, + tok_size)) || + (tok_size != strlen (rh->url) ) ) + continue; + found = true; + /* The URL is a match! What we now do depends on the method. */ + if (0 == strcasecmp (method, + MHD_HTTP_METHOD_OPTIONS)) + { + return TALER_MHD_reply_cors_preflight (connection); + } + GNUNET_assert (NULL != rh->method); + if (0 != strcasecmp (method, + rh->method)) + { + found = true; + continue; + } + /* cache to avoid the loop next time */ + rc->rh = rh; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handler found for %s '%s'\n", + method, + url); + return MHD_YES; + } + + if (found) + { + /* we found a matching address, but the method is wrong */ + struct MHD_Response *reply; + MHD_RESULT ret; + char *allowed = NULL; + + GNUNET_break_op (0); + for (unsigned int i = 0; NULL != handlers[i].url; i++) + { + struct TEKT_RequestHandler *rh = &handlers[i]; + + if ( (0 != strncmp (tok, + rh->url, + tok_size)) || + (tok_size != strlen (rh->url) ) ) + continue; + if (NULL == allowed) + { + allowed = GNUNET_strdup (rh->method); + } + else + { + char *tmp; + + GNUNET_asprintf (&tmp, + "%s, %s", + allowed, + rh->method); + GNUNET_free (allowed); + allowed = tmp; + } + if (0 == strcasecmp (rh->method, + MHD_HTTP_METHOD_GET)) + { + char *tmp; + + GNUNET_asprintf (&tmp, + "%s, %s", + allowed, + MHD_HTTP_METHOD_HEAD); + GNUNET_free (allowed); + allowed = tmp; + } + } + reply = TALER_MHD_make_error (TALER_EC_GENERIC_METHOD_INVALID, + method); + GNUNET_break (MHD_YES == + MHD_add_response_header (reply, + MHD_HTTP_HEADER_ALLOW, + allowed)); + GNUNET_free (allowed); + ret = MHD_queue_response (connection, + MHD_HTTP_METHOD_NOT_ALLOWED, + reply); + MHD_destroy_response (reply); + return ret; + } + } + + /* No handler matches, generate not found */ + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + url); +} + + +/** + * Load configuration parameters for the exchange + * server into the corresponding global variables. + * + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +exchange_serve_process_config (void) +{ + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (TEKT_cfg, + "exchange", + "BASE_URL", + &TEKT_base_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "BASE_URL"); + return GNUNET_SYSERR; + } + if (! TALER_url_valid_charset (TEKT_base_url)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "BASE_URL", + "invalid URL"); + return GNUNET_SYSERR; + } + + return GNUNET_OK; +} + + +/** + * Function run on shutdown. + * + * @param cls NULL + */ +static void +do_shutdown (void *cls) +{ + struct MHD_Daemon *mhd; + struct ProofRequestState *rs; + + (void) cls; + while (NULL != (rs = rs_head)) + { + GNUNET_CONTAINER_DLL_remove (rs_head, + rs_tail, + rs); + rs->logic->proof_cancel (rs->ph); + MHD_resume_connection (rs->rc->connection); + GNUNET_free (rs); + } + if (NULL != ih) + { + ih_logic->initiate_cancel (ih); + ih = NULL; + } + kyc_webhook_cleanup (); + TALER_KYCLOGIC_kyc_done (); + mhd = TALER_MHD_daemon_stop (); + if (NULL != mhd) + MHD_stop_daemon (mhd); + if (NULL != TEKT_curl_ctx) + { + GNUNET_CURL_fini (TEKT_curl_ctx); + TEKT_curl_ctx = NULL; + } + if (NULL != exchange_curl_rc) + { + GNUNET_CURL_gnunet_rc_destroy (exchange_curl_rc); + exchange_curl_rc = NULL; + } + TALER_TEMPLATING_done (); +} + + +/** + * Function called with the result of a KYC initiation + * operation. + * + * @param cls closure + * @param ec #TALER_EC_NONE on success + * @param redirect_url set to where to redirect the user on success, NULL on failure + * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown + * @param error_msg_hint set to additional details to return to user, NULL on success + */ +static void +initiate_cb ( + void *cls, + enum TALER_ErrorCode ec, + const char *redirect_url, + const char *provider_user_id, + const char *provider_legitimization_id, + const char *error_msg_hint) +{ + (void) cls; + ih = NULL; + if (TALER_EC_NONE != ec) + { + fprintf (stderr, + "Failed to start KYC process: %s (#%d)\n", + error_msg_hint, + ec); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + { + char *s; + + s = GNUNET_STRINGS_data_to_string_alloc (&cmd_line_h_payto, + sizeof (cmd_line_h_payto)); + if (NULL != provider_user_id) + { + fprintf (stdout, + "Visit `%s' to begin KYC process.\nAlso use: taler-exchange-kyc-tester -w -u '%s' -U '%s' -p %s\n", + redirect_url, + provider_user_id, + provider_legitimization_id, + s); + } + else + { + fprintf (stdout, + "Visit `%s' to begin KYC process.\nAlso use: taler-exchange-kyc-tester -w -U '%s' -p %s\n", + redirect_url, + provider_legitimization_id, + s); + } + GNUNET_free (s); + } + GNUNET_free (cmd_provider_user_id); + GNUNET_free (cmd_provider_legitimization_id); + if (NULL != provider_user_id) + cmd_provider_user_id = GNUNET_strdup (provider_user_id); + if (NULL != provider_legitimization_id) + cmd_provider_legitimization_id = GNUNET_strdup (provider_legitimization_id); + if (! run_webservice) + GNUNET_SCHEDULER_shutdown (); +} + + +/** + * 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; + + (void) cls; + (void) args; + (void ) cfgfile; + if (GNUNET_OK != + TALER_TEMPLATING_init ("exchange")) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not load templates. Installation broken.\n"); + return; + } + if (print_h_payto) + { + char *s; + + s = GNUNET_STRINGS_data_to_string_alloc (&cmd_line_h_payto, + sizeof (cmd_line_h_payto)); + fprintf (stdout, + "%s\n", + s); + GNUNET_free (s); + } + TALER_MHD_setup (TALER_MHD_GO_NONE); + TEKT_cfg = config; + GNUNET_SCHEDULER_add_shutdown (&do_shutdown, + NULL); + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_init (config)) + { + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + exchange_serve_process_config ()) + { + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + global_ret = EXIT_SUCCESS; + if (NULL != requirements) + { + struct TALER_KYCLOGIC_ProviderDetails *pd; + enum TALER_KYCLOGIC_KycUserType ut; + + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_user_type_from_string (ut_s, + &ut)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid user type specified ('-i')\n"); + global_ret = EXIT_INVALIDARGUMENT; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_KYCLOGIC_requirements_to_logic (requirements, + ut, + &ih_logic, + &pd, + &provider_section_name)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not initiate KYC for requirements `%s' (configuration error?)\n", + requirements); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + ih = ih_logic->initiate (ih_logic->cls, + pd, + &cmd_line_h_payto, + kyc_row_id, + &initiate_cb, + NULL); + GNUNET_break (NULL != ih); + } + if (run_webservice) + { + TEKT_curl_ctx + = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &exchange_curl_rc); + if (NULL == TEKT_curl_ctx) + { + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + exchange_curl_rc = GNUNET_CURL_gnunet_rc_create (TEKT_curl_ctx); + fh = TALER_MHD_bind (TEKT_cfg, + "exchange", + &serve_port); + if ( (0 == serve_port) && + (-1 == fh) ) + { + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting daemon on port %u\n", + (unsigned int) serve_port); + mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME + | MHD_USE_PIPE_FOR_SHUTDOWN + | MHD_USE_DEBUG | MHD_USE_DUAL_STACK + | MHD_USE_TCP_FASTOPEN, + (-1 == fh) ? serve_port : 0, + NULL, NULL, + &handle_mhd_request, NULL, + MHD_OPTION_LISTEN_SOCKET, + fh, + MHD_OPTION_EXTERNAL_LOGGER, + &TALER_MHD_handle_logs, + NULL, + MHD_OPTION_NOTIFY_COMPLETED, + &handle_mhd_completion_callback, + NULL, + MHD_OPTION_END); + if (NULL == mhd) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to launch HTTP service. Is the port in use?\n"); + GNUNET_SCHEDULER_shutdown (); + return; + } + TALER_MHD_daemon_start (mhd); + } +} + + +/** + * The main function of the taler-exchange-httpd server ("the exchange"). + * + * @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) +{ + const struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_help ( + "tool to test KYC provider integrations"), + GNUNET_GETOPT_option_flag ( + 'P', + "print-payto-hash", + "output the hash of the payto://-URI", + &print_h_payto), + GNUNET_GETOPT_option_uint ( + 'r', + "rowid", + "NUMBER", + "override row ID to use in simulation (default: 42)", + &kyc_row_id), + GNUNET_GETOPT_option_flag ( + 'w', + "run-webservice", + "run the integrated HTTP service", + &run_webservice), + GNUNET_GETOPT_option_string ( + 'R', + "requirements", + "CHECKS", + "initiate KYC check for the given list of (space-separated) checks", + &requirements), + GNUNET_GETOPT_option_string ( + 'i', + "identify", + "USERTYPE", + "self-identify as USERTYPE 'business' or 'individual' (defaults to 'individual')", + &requirements), + GNUNET_GETOPT_option_string ( + 'u', + "user", + "ID", + "use the given provider user ID (overridden if -i is also used)", + &cmd_provider_user_id), + GNUNET_GETOPT_option_string ( + 'U', + "legitimization", + "ID", + "use the given provider legitimization ID (overridden if -i is also used)", + &cmd_provider_legitimization_id), + GNUNET_GETOPT_option_base32_fixed_size ( + 'p', + "payto-hash", + "HASH", + "base32 encoding of the hash of a payto://-URI to use for the account (otherwise a random value will be used)", + &cmd_line_h_payto, + sizeof (cmd_line_h_payto)), + GNUNET_GETOPT_OPTION_END + }; + enum GNUNET_GenericReturnValue ret; + + TALER_OS_init (); + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &cmd_line_h_payto, + sizeof (cmd_line_h_payto)); + ret = GNUNET_PROGRAM_run (argc, argv, + "taler-exchange-kyc-tester", + "tool to test KYC provider integrations", + options, + &run, NULL); + if (GNUNET_SYSERR == ret) + return EXIT_INVALIDARGUMENT; + if (GNUNET_NO == ret) + return EXIT_SUCCESS; + return global_ret; +} + + +/* end of taler-exchange-kyc-tester.c */ |