add code to sanity-check KYC configuration and KYC decisions

This commit is contained in:
Christian Grothoff 2023-02-16 16:37:38 +01:00
parent 4d2d0473c3
commit 87a78c6f8c
No known key found for this signature in database
GPG Key ID: 939E6BE1E29FC3CC
4 changed files with 277 additions and 77 deletions

@ -1 +1 @@
Subproject commit 3a616a04f1cd946bf0641b54cd71f1b858174f74 Subproject commit 1ec4596bf4925ee24fc06d3e74d2a553b8239870

View File

@ -26,14 +26,138 @@
#include <pthread.h> #include <pthread.h>
#include "taler_json_lib.h" #include "taler_json_lib.h"
#include "taler_mhd_lib.h" #include "taler_mhd_lib.h"
#include "taler_kyclogic_lib.h"
#include "taler_signatures.h" #include "taler_signatures.h"
#include "taler-exchange-httpd_responses.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 MHD_RESULT
@ -43,31 +167,27 @@ TEH_handler_post_aml_decision (
const json_t *root) const json_t *root)
{ {
struct MHD_Connection *connection = rc->connection; struct MHD_Connection *connection = rc->connection;
const char *justification; struct DecisionContext dc = {
struct GNUNET_TIME_Timestamp decision_time; .officer_pub = officer_pub
struct TALER_Amount new_threshold; };
struct TALER_PaytoHashP h_payto;
uint32_t new_state32; uint32_t new_state32;
enum TALER_AmlDecisionState new_state;
struct TALER_AmlOfficerSignatureP officer_sig;
json_t *kyc_requirements = NULL;
struct GNUNET_JSON_Specification spec[] = { struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_fixed_auto ("officer_sig", GNUNET_JSON_spec_fixed_auto ("officer_sig",
&officer_sig), &dc.officer_sig),
GNUNET_JSON_spec_fixed_auto ("h_payto", GNUNET_JSON_spec_fixed_auto ("h_payto",
&h_payto), &dc.h_payto),
TALER_JSON_spec_amount ("new_threshold", TALER_JSON_spec_amount ("new_threshold",
TEH_currency, TEH_currency,
&new_threshold), &dc.new_threshold),
GNUNET_JSON_spec_string ("justification", GNUNET_JSON_spec_string ("justification",
&justification), &dc.justification),
GNUNET_JSON_spec_timestamp ("decision_time", GNUNET_JSON_spec_timestamp ("decision_time",
&decision_time), &dc.decision_time),
GNUNET_JSON_spec_uint32 ("new_state", GNUNET_JSON_spec_uint32 ("new_state",
&new_state32), &new_state32),
GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_json ("kyc_requirements", GNUNET_JSON_spec_json ("kyc_requirements",
&kyc_requirements), &dc.kyc_requirements),
NULL), NULL),
GNUNET_JSON_spec_end () GNUNET_JSON_spec_end ()
}; };
@ -86,17 +206,17 @@ TEH_handler_post_aml_decision (
return MHD_YES; /* failure */ 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]++; TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
if (GNUNET_OK != if (GNUNET_OK !=
TALER_officer_aml_decision_verify (justification, TALER_officer_aml_decision_verify (dc.justification,
decision_time, dc.decision_time,
&new_threshold, &dc.new_threshold,
&h_payto, &dc.h_payto,
new_state, dc.new_state,
kyc_requirements, dc.kyc_requirements,
officer_pub, dc.officer_pub,
&officer_sig)) &dc.officer_sig))
{ {
GNUNET_break_op (0); GNUNET_break_op (0);
return TALER_MHD_reply_with_error ( return TALER_MHD_reply_with_error (
@ -106,62 +226,67 @@ TEH_handler_post_aml_decision (
NULL); NULL);
} }
// FIXME: check kyc_requirements is well-formed! if (NULL != dc.kyc_requirements)
{ {
enum GNUNET_DB_QueryStatus qs; size_t index;
struct GNUNET_TIME_Timestamp last_date; json_t *elem;
bool invalid_officer;
unsigned int retries_left = MAX_RETRIES;
do { if (! json_is_array (dc.kyc_requirements))
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)
{ {
GNUNET_break_op (0); GNUNET_break_op (0);
GNUNET_JSON_parse_free (spec);
return TALER_MHD_reply_with_error ( return TALER_MHD_reply_with_error (
connection, connection,
MHD_HTTP_FORBIDDEN, MHD_HTTP_BAD_REQUEST,
TALER_EC_EXCHANGE_AML_DECISION_INVALID_OFFICER, TALER_EC_GENERIC_PARAMETER_MALFORMED,
NULL); "kyc_requirements must be an array");
} }
if (GNUNET_TIME_timestamp_cmp (last_date,
>=, json_array_foreach (dc.kyc_requirements, index, elem)
decision_time)) {
const char *val;
if (! json_is_string (elem))
{ {
GNUNET_break_op (0); GNUNET_break_op (0);
GNUNET_JSON_parse_free (spec);
return TALER_MHD_reply_with_error ( return TALER_MHD_reply_with_error (
connection, connection,
MHD_HTTP_CONFLICT, MHD_HTTP_BAD_REQUEST,
TALER_EC_EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT, TALER_EC_GENERIC_PARAMETER_MALFORMED,
NULL); "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 ( return TALER_MHD_reply_static (
connection, connection,
MHD_HTTP_NO_CONTENT, MHD_HTTP_NO_CONTENT,

View File

@ -296,6 +296,20 @@ TALER_KYCLOGIC_kyc_get_details (
void *cb_cls); 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. * Obtain the provider logic for a given set of @a requirements.
* *

View File

@ -21,6 +21,12 @@
#include "platform.h" #include "platform.h"
#include "taler_kyclogic_lib.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. * 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; 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;
}
/** /**
* Load KYC logic plugin. * Load KYC logic plugin.
* *
@ -331,9 +352,8 @@ add_check (const char *check)
/** /**
* Parse list of checks from @a checks and build an * Parse list of checks from @a checks and build an array of aliases into the
* array of aliases into the global checks array * global checks array in @a provided_checks.
* in @a provided_checks.
* *
* @param[in,out] checks list of checks; clobbered * @param[in,out] checks list of checks; clobbered
* @param[out] p_checks where to put array of aliases * @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, GNUNET_array_append (kyc_triggers,
num_kyc_triggers, num_kyc_triggers,
kt); 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; return GNUNET_OK;
} }
@ -614,7 +657,7 @@ struct SectionContext
* @param section name of the section * @param section name of the section
*/ */
static void static void
handle_section (void *cls, handle_provider_section (void *cls,
const char *section) const char *section)
{ {
struct SectionContext *sc = cls; struct SectionContext *sc = cls;
@ -629,6 +672,21 @@ handle_section (void *cls,
sc->result = false; sc->result = false;
return; 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, if (0 == strncasecmp (section,
"kyc-legitimization-", "kyc-legitimization-",
strlen ("kyc-legitimization-"))) strlen ("kyc-legitimization-")))
@ -680,7 +738,10 @@ TALER_KYCLOGIC_kyc_init (const struct GNUNET_CONFIGURATION_Handle *cfg)
}; };
GNUNET_CONFIGURATION_iterate_sections (cfg, GNUNET_CONFIGURATION_iterate_sections (cfg,
&handle_section, &handle_provider_section,
&sc);
GNUNET_CONFIGURATION_iterate_sections (cfg,
&handle_trigger_section,
&sc); &sc);
if (! sc.result) if (! sc.result)
{ {