From 87a78c6f8ce1d50a5f61eb5c3f222cdef0b635ee Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Thu, 16 Feb 2023 16:37:38 +0100 Subject: [PATCH] add code to sanity-check KYC configuration and KYC decisions --- contrib/gana | 2 +- .../taler-exchange-httpd_aml-decision.c | 265 +++++++++++++----- src/include/taler_kyclogic_lib.h | 14 + src/kyclogic/kyclogic_api.c | 73 ++++- 4 files changed, 277 insertions(+), 77 deletions(-) diff --git a/contrib/gana b/contrib/gana index 3a616a04f..1ec4596bf 160000 --- a/contrib/gana +++ b/contrib/gana @@ -1 +1 @@ -Subproject commit 3a616a04f1cd946bf0641b54cd71f1b858174f74 +Subproject commit 1ec4596bf4925ee24fc06d3e74d2a553b8239870 diff --git a/src/exchange/taler-exchange-httpd_aml-decision.c b/src/exchange/taler-exchange-httpd_aml-decision.c index 0f586279b..0fd58b9ec 100644 --- a/src/exchange/taler-exchange-httpd_aml-decision.c +++ b/src/exchange/taler-exchange-httpd_aml-decision.c @@ -26,14 +26,138 @@ #include #include "taler_json_lib.h" #include "taler_mhd_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_signatures.h" #include "taler-exchange-httpd_responses.h" /** - * How often do we try the DB operation at most? + * Closure for #make_aml_decision() */ -#define MAX_RETRIES 10 +struct DecisionContext +{ + /** + * Justification given for the decision. + */ + const char *justification; + + /** + * When was the decision taken. + */ + struct GNUNET_TIME_Timestamp decision_time; + + /** + * New threshold for revising the decision. + */ + struct TALER_Amount new_threshold; + + /** + * Hash of payto://-URI of affected account. + */ + struct TALER_PaytoHashP h_payto; + + /** + * New AML state. + */ + enum TALER_AmlDecisionState new_state; + + /** + * Signature affirming the decision. + */ + struct TALER_AmlOfficerSignatureP officer_sig; + + /** + * Public key of the AML officer. + */ + const struct TALER_AmlOfficerPublicKeyP *officer_pub; + + /** + * KYC requirements imposed, NULL for none. + */ + json_t *kyc_requirements; + +}; + + +/** + * Function implementing AML decision database transaction. + * + * Runs the transaction logic; IF it returns a non-error code, the + * transaction logic MUST NOT queue a MHD response. IF it returns an hard + * error, the transaction logic MUST queue a MHD response and set @a mhd_ret. + * IF it returns the soft error code, the function MAY be called again to + * retry and MUST not queue a MHD response. + * + * @param cls closure with a `struct DecisionContext` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +make_aml_decision (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct DecisionContext *dc = cls; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp last_date; + bool invalid_officer; + + qs = TEH_plugin->insert_aml_decision (TEH_plugin->cls, + &dc->h_payto, + &dc->new_threshold, + dc->new_state, + dc->decision_time, + dc->justification, + dc->kyc_requirements, + dc->officer_pub, + &dc->officer_sig, + &invalid_officer, + &last_date); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_aml_decision"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; + } + if (invalid_officer) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_AML_DECISION_INVALID_OFFICER, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (GNUNET_TIME_timestamp_cmp (last_date, + >=, + dc->decision_time)) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (NULL != dc->kyc_requirements) + { + // FIXME: act on these! + } + + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} MHD_RESULT @@ -43,31 +167,27 @@ TEH_handler_post_aml_decision ( const json_t *root) { struct MHD_Connection *connection = rc->connection; - const char *justification; - struct GNUNET_TIME_Timestamp decision_time; - struct TALER_Amount new_threshold; - struct TALER_PaytoHashP h_payto; + struct DecisionContext dc = { + .officer_pub = officer_pub + }; uint32_t new_state32; - enum TALER_AmlDecisionState new_state; - struct TALER_AmlOfficerSignatureP officer_sig; - json_t *kyc_requirements = NULL; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("officer_sig", - &officer_sig), + &dc.officer_sig), GNUNET_JSON_spec_fixed_auto ("h_payto", - &h_payto), + &dc.h_payto), TALER_JSON_spec_amount ("new_threshold", TEH_currency, - &new_threshold), + &dc.new_threshold), GNUNET_JSON_spec_string ("justification", - &justification), + &dc.justification), GNUNET_JSON_spec_timestamp ("decision_time", - &decision_time), + &dc.decision_time), GNUNET_JSON_spec_uint32 ("new_state", &new_state32), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_json ("kyc_requirements", - &kyc_requirements), + &dc.kyc_requirements), NULL), GNUNET_JSON_spec_end () }; @@ -86,17 +206,17 @@ TEH_handler_post_aml_decision ( return MHD_YES; /* failure */ } } - new_state = (enum TALER_AmlDecisionState) new_state32; + dc.new_state = (enum TALER_AmlDecisionState) new_state32; TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != - TALER_officer_aml_decision_verify (justification, - decision_time, - &new_threshold, - &h_payto, - new_state, - kyc_requirements, - officer_pub, - &officer_sig)) + TALER_officer_aml_decision_verify (dc.justification, + dc.decision_time, + &dc.new_threshold, + &dc.h_payto, + dc.new_state, + dc.kyc_requirements, + dc.officer_pub, + &dc.officer_sig)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error ( @@ -106,62 +226,67 @@ TEH_handler_post_aml_decision ( NULL); } - // FIXME: check kyc_requirements is well-formed! + if (NULL != dc.kyc_requirements) { - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Timestamp last_date; - bool invalid_officer; - unsigned int retries_left = MAX_RETRIES; + size_t index; + json_t *elem; - do { - qs = TEH_plugin->insert_aml_decision (TEH_plugin->cls, - &h_payto, - &new_threshold, - new_state, - decision_time, - justification, - kyc_requirements, - officer_pub, - &officer_sig, - &invalid_officer, - &last_date); - if (0 == --retries_left) - break; - } while (GNUNET_DB_STATUS_SOFT_ERROR == qs); - if (qs <= 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "add aml_decision"); - } - if (NULL != kyc_requirements) - { - // FIXME: act on these! - } - - if (invalid_officer) + if (! json_is_array (dc.kyc_requirements)) { GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error ( connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_AML_DECISION_INVALID_OFFICER, - NULL); + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "kyc_requirements must be an array"); } - if (GNUNET_TIME_timestamp_cmp (last_date, - >=, - decision_time)) + + json_array_foreach (dc.kyc_requirements, index, elem) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT, - NULL); + const char *val; + + if (! json_is_string (elem)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "kyc_requirements array members must be strings"); + } + val = json_string_value (elem); + if (GNUNET_SYSERR == + TALER_KYCLOGIC_check_satisfiable (val)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_AML_DECISION_UNKNOWN_CHECK, + val); + } } } + + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "make-aml-decision", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &make_aml_decision, + &dc)) + { + GNUNET_JSON_parse_free (spec); + return mhd_ret; + } + } + GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_static ( connection, MHD_HTTP_NO_CONTENT, diff --git a/src/include/taler_kyclogic_lib.h b/src/include/taler_kyclogic_lib.h index a629543a0..065f25618 100644 --- a/src/include/taler_kyclogic_lib.h +++ b/src/include/taler_kyclogic_lib.h @@ -296,6 +296,20 @@ TALER_KYCLOGIC_kyc_get_details ( void *cb_cls); +/** + * Check if a given @a check_name is a legal name (properly + * configured) and can be satisfied in principle. + * + * @param logic_name name of the logic to match + * @return #GNUNET_OK if the check can be satisfied, + * #GNUNET_NO if the check can never be satisfied, + * #GNUNET_SYSERR if the type of the check is unknown + */ +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_check_satisfiable ( + const char *check_name); + + /** * Obtain the provider logic for a given set of @a requirements. * diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c index 6d92fce68..b82a6c065 100644 --- a/src/kyclogic/kyclogic_api.c +++ b/src/kyclogic/kyclogic_api.c @@ -21,6 +21,12 @@ #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. */ @@ -265,6 +271,21 @@ TALER_KYCLOGIC_kyc_user_type2s (enum TALER_KYCLOGIC_KycUserType ut) } +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_check_satisfiable ( + const char *check_name) +{ + for (unsigned int i = 0; iname)) + return GNUNET_OK; + if (0 == strcmp (check_name, + KYC_CHECK_IMPOSSIBLE)) + return GNUNET_NO; + return GNUNET_SYSERR; +} + + /** * Load KYC logic plugin. * @@ -331,9 +352,8 @@ add_check (const char *check) /** - * Parse list of checks from @a checks and build an - * array of aliases into the global checks array - * in @a provided_checks. + * 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 @@ -585,6 +605,29 @@ add_trigger (const struct GNUNET_CONFIGURATION_Handle *cfg, GNUNET_array_append (kyc_triggers, num_kyc_triggers, kt); + for (unsigned int i = 0; inum_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; } @@ -614,8 +657,8 @@ struct SectionContext * @param section name of the section */ static void -handle_section (void *cls, - const char *section) +handle_provider_section (void *cls, + const char *section) { struct SectionContext *sc = cls; @@ -629,6 +672,21 @@ handle_section (void *cls, 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-"))) @@ -680,7 +738,10 @@ TALER_KYCLOGIC_kyc_init (const struct GNUNET_CONFIGURATION_Handle *cfg) }; GNUNET_CONFIGURATION_iterate_sections (cfg, - &handle_section, + &handle_provider_section, + &sc); + GNUNET_CONFIGURATION_iterate_sections (cfg, + &handle_trigger_section, &sc); if (! sc.result) {