exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 8ad5457d51d5089f4f6c7dfe5a81b7210dd28110
parent a0c3b23d05691d6bf566b928a563bbaaf8d8e0e8
Author: Christian Grothoff <christian@grothoff.org>
Date:   Mon,  9 Jun 2025 00:06:21 +0200

add test and fix bugs for sanction list check (for #9053)

Diffstat:
Msrc/exchange/exchange.conf | 10+++++-----
Msrc/exchange/taler-exchange-sanctionscheck.c | 58+++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/exchange/test_taler_exchange_httpd.sh | 5++---
Msrc/exchangedb/exchange_do_insert_sanction_list_hit.sql | 5+++++
Msrc/exchangedb/pg_insert_sanction_list_hit.c | 4++--
Msrc/kyclogic/kyclogic_sanctions.c | 119++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/kyclogic/taler-exchange-helper-measure-test-form | 10+++++-----
Msrc/testing/Makefile.am | 7++++++-
Asrc/testing/sanction-list.json | 3+++
Msrc/testing/test-sanctions.sh | 38+++++++++++++++++---------------------
Msrc/testing/test_kyc_api.c | 2+-
Msrc/testing/test_sanctions.conf | 2+-
12 files changed, 164 insertions(+), 99 deletions(-)

diff --git a/src/exchange/exchange.conf b/src/exchange/exchange.conf @@ -138,17 +138,17 @@ MIN_ROW_FILENAME = ${TALER_CACHE_HOME}sanctionscheck-offset.bin # Sanction list match rating that must be exceeded for an automated # freeze of the account (without manual investigation first). # Both this and the FREEZE_CONFIDENCE_LIMIT must be met. -FREEZE_RATING_LIMIT = 0.95 +FREEZE_RATING_LIMIT = 0.9 # Sanction list confidence that must be exceeded for an automated # freeze of the account (without manual investigation first). # Both this and the FREEZE_RATING_LIMIT must be met. -FREEZE_CONFIDENCE_LIMIT = 0.95 +FREEZE_CONFIDENCE_LIMIT = 0.9 # Value that the rating divided by the confidence must exceed to -# trigger a (manual) investigation. At 0.95 (default), a modest +# trigger a (manual) investigation. At 0.95, a modest # 0.8 match with low 0.5 confidence will trigger (1.6), but a # good 0.9 match with a super-high 1.0 confidence would not (0.9 < 0.95). # OTOH, a bad 0.2 match with super-low 0.1 confidence would again # trigger (2.0 > 0.95). -INVESTIGATION_LIMIT = 0.95 -\ No newline at end of file +INVESTIGATION_LIMIT = 0.9 +\ No newline at end of file diff --git a/src/exchange/taler-exchange-sanctionscheck.c b/src/exchange/taler-exchange-sanctionscheck.c @@ -157,18 +157,18 @@ static bool in_transaction; /** * Match quality needed for instantly freezing an account. */ -static float freeze_rating_limit = 0.95; +static float freeze_rating_limit = 0.9; /** * Match confidence needed for instantly freezing an account. */ -static float freeze_confidence_limit = 0.95; +static float freeze_confidence_limit = 0.9; /** * Rating/confidence threshold that must be passed to begin * an investigation. */ -static float investigation_limit = 0.95; +static float investigation_limit = 0.9; /** * Write @a min_row_id to @a min_row_fd. @@ -208,9 +208,9 @@ shutdown_task (void *cls) struct Account *acc; (void) cls; - sync_row (); if (-1 != min_row_fd) { + sync_row (); GNUNET_break (0 == close (min_row_fd)); min_row_fd = -1; } @@ -284,6 +284,17 @@ sanction_cb (void *cls, bool freeze = false; bool investigate = false; + if (TALER_EC_NONE != ec) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Error %s (%d) when analyzing record\n", + TALER_ErrorCode_get_hint (ec), + (int) ec); + db_plugin->rollback (db_plugin->cls); + global_ret = 1; + GNUNET_SCHEDULER_shutdown (); + return; + } if ( (rating > (double) freeze_rating_limit) && (confidence > (double) freeze_confidence_limit) ) { @@ -294,6 +305,16 @@ sanction_cb (void *cls, { investigate = true; } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Best match is %f/%f at `%s' will %s\n", + rating, + confidence, + best_match, + freeze || investigate + ? (freeze + ? "freeze" + : "investigate") + : "do nothing"); if (freeze || investigate) { static const char *freeze_event[] = { @@ -320,9 +341,10 @@ sanction_cb (void *cls, "INVESTIGATION_PENDING"), GNUNET_JSON_pack_string ("AML_INVESTIGATION_TIGGER", "SANCTION_LIST_MATCH")); - GNUNET_assert (0 == - json_object_update_missing (properties, - acc->properties)); + if (NULL != acc->properties) + GNUNET_assert (0 == + json_object_update_missing (properties, + acc->properties)); qs = db_plugin->insert_sanction_list_hit (db_plugin->cls, &acc->h_payto, investigate, @@ -420,6 +442,14 @@ account_cb (void *cls, (unsigned long long) row_id); return true; } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found KYC data %llu\n", + (unsigned long long) row_id); +#if DEBUG + json_dumpf (attributes, + stderr, + JSON_INDENT (2)); +#endif acc = GNUNET_new (struct Account); acc->row_id = row_id; acc->h_payto = *h_payto; @@ -697,6 +727,7 @@ run (void *cls, "exchange-sanctionscheck", "MIN_ROW_FILENAME"); global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); return; } if (reset && @@ -708,6 +739,18 @@ run (void *cls, min_row_fn); GNUNET_free (min_row_fn); global_ret = EXIT_NOPERMISSION; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + GNUNET_DISK_directory_create_for_file (min_row_fn)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + min_row_fn); + GNUNET_free (min_row_fn); + global_ret = EXIT_NOPERMISSION; + GNUNET_SCHEDULER_shutdown (); return; } min_row_fd = open (min_row_fn, @@ -720,6 +763,7 @@ run (void *cls, min_row_fn); GNUNET_free (min_row_fn); global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); return; } if (sizeof (r) != diff --git a/src/exchange/test_taler_exchange_httpd.sh b/src/exchange/test_taler_exchange_httpd.sh @@ -35,7 +35,7 @@ PREFIX= taler-exchange-dbinit -c test_taler_exchange_httpd.conf &> /dev/null || exit 77 # Run Exchange HTTPD (in background) $PREFIX taler-exchange-httpd -c test_taler_exchange_httpd.conf 2> test-exchange.log & - +EPID=$! # Give HTTP time to start for n in `seq 1 100` @@ -74,8 +74,7 @@ echo -n . cat test_taler_exchange_httpd.get | grep -v ^\# | awk '{ print "curl -H \"Accept: */plain\" http://localhost:8081" $1 }' | bash &> /dev/null echo " DONE" -# $! is the last backgrounded process, hence the exchange -kill -TERM $! +kill -TERM "${EPID}" wait $! # Return status code from exchange for this script exit $? diff --git a/src/exchangedb/exchange_do_insert_sanction_list_hit.sql b/src/exchangedb/exchange_do_insert_sanction_list_hit.sql @@ -32,6 +32,11 @@ DECLARE ini_event TEXT; BEGIN +-- Disable all previous legitimization outcomes. +UPDATE legitimization_outcomes + SET is_active=FALSE + WHERE h_payto=in_h_normalized_payto; + INSERT INTO legitimization_outcomes (h_payto ,decision_time diff --git a/src/exchangedb/pg_insert_sanction_list_hit.c b/src/exchangedb/pg_insert_sanction_list_hit.c @@ -76,8 +76,8 @@ TEH_PG_insert_sanction_list_hit ( PREPARE (pg, "do_insert_sanction_list_hit", "SELECT" - " out_outcome_serial_id" - " FROM exchange_insert_sanction_list_hit" + " out_outcome_serial_id AS outcome_serial_id" + " FROM exchange_do_insert_sanction_list_hit" "($1,$2,$3,$4,$5,$6,$7,$8);"); qs = GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, diff --git a/src/kyclogic/kyclogic_sanctions.c b/src/kyclogic/kyclogic_sanctions.c @@ -51,7 +51,7 @@ struct TALER_KYCLOGIC_EvaluationEntry /** * Buffer with data we need to send to the helper. */ - void *write_buf; + char *write_buf; /** * Total length of @e write_buf. @@ -167,7 +167,10 @@ fail_hard (struct TALER_KYCLOGIC_SanctionRater *sr) NULL, 1.0, 0.0); - free (ee->write_buf); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to send %u bytes to child\n", + (unsigned int) (ee->write_size - ee->write_pos)); + GNUNET_free (ee->write_buf); GNUNET_free (ee); } } @@ -192,6 +195,7 @@ process_buffer (struct TALER_KYCLOGIC_SanctionRater *sr) if ( (NULL == end) && (sr->read_pos < 2048) ) return true; + end++; buf_len = end - sr->read_buf; while (0 != buf_len) { @@ -204,9 +208,14 @@ process_buffer (struct TALER_KYCLOGIC_SanctionRater *sr) nl = memchr (buf, '\n', buf_len); - GNUNET_assert (NULL != nl); + if (NULL == nl) + { + /* no newline in 2048 bytes? not allowed */ + GNUNET_break (0); + return false; + } *nl = '\0'; - line_len = nl - buf; + line_len = nl - buf + 1; if (3 != sscanf (buf, "%lf %lf %1023s", @@ -231,7 +240,7 @@ process_buffer (struct TALER_KYCLOGIC_SanctionRater *sr) best_match, rating, confidence); - free (ee->write_buf); + GNUNET_free (ee->write_buf); GNUNET_free (ee); } buf += line_len; @@ -347,54 +356,56 @@ write_cb (void *cls) ssize_t ret; sr->write_task = NULL; - while (ee->write_size > ee->write_pos) + while ( (NULL != ee) && + (ee->write_size == ee->write_pos) ) + ee = ee->prev; + while (NULL != ee) { - ret = GNUNET_DISK_file_write (sr->chld_stdin, - ee->write_buf + ee->write_pos, - ee->write_size - ee->write_pos); - if (ret < 0) + while (ee->write_size > ee->write_pos) { - if ( (EAGAIN != errno) && - (EINTR != errno) ) - GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, - "write"); - break; + ret = GNUNET_DISK_file_write (sr->chld_stdin, + ee->write_buf + ee->write_pos, + ee->write_size - ee->write_pos); + if (ret < 0) + { + if ( (EAGAIN != errno) && + (EINTR != errno) ) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, + "write"); + /* helper must have died */ + fail_hard (sr); + return; + } + break; + } + if (0 == ret) + { + GNUNET_break (0); + break; + } + GNUNET_assert (ee->write_size >= ee->write_pos + ret); + ee->write_pos += ret; } - if (0 == ret) + if ( (ee->write_size > ee->write_pos) && + ( (EAGAIN == errno) || + (EWOULDBLOCK == errno) || + (EINTR == errno) ) ) { - GNUNET_break (0); - break; - } - GNUNET_assert (ee->write_size >= ee->write_pos + ret); - ee->write_pos += ret; - } - if (ee->write_size == ee->write_pos) - { - free (ee->write_buf); - GNUNET_CONTAINER_DLL_remove (sr->ee_head, - sr->ee_tail, - ee); - GNUNET_free (ee); - ee = sr->ee_tail; - if (NULL == ee) + sr->write_task + = GNUNET_SCHEDULER_add_write_file ( + GNUNET_TIME_UNIT_FOREVER_REL, + sr->chld_stdin, + &write_cb, + sr); return; - } - if ( (ee->write_size > ee->write_pos) && - ( (EAGAIN == errno) || - (EWOULDBLOCK == errno) || - (EINTR == errno) ) ) - { - sr->write_task - = GNUNET_SCHEDULER_add_write_file ( - GNUNET_TIME_UNIT_FOREVER_REL, - sr->chld_stdin, - &write_cb, - sr); - return; - } - /* helper must have died */ - GNUNET_break (0); - fail_hard (sr); + } + if (ee->write_size == ee->write_pos) + { + GNUNET_free (ee->write_buf); + ee = ee->prev; + } + } /* while (NULL != ee) */ } @@ -485,6 +496,7 @@ TALER_KYCLOGIC_sanction_rater_eval (struct TALER_KYCLOGIC_SanctionRater *sr, void *cb_cls) { struct TALER_KYCLOGIC_EvaluationEntry *ee; + char *js; if (NULL == sr->read_task) return NULL; @@ -494,12 +506,13 @@ TALER_KYCLOGIC_sanction_rater_eval (struct TALER_KYCLOGIC_SanctionRater *sr, GNUNET_CONTAINER_DLL_insert (sr->ee_head, sr->ee_tail, ee); - ee->write_buf = json_dumps (attributes, - JSON_COMPACT); + js = json_dumps (attributes, + JSON_COMPACT); + GNUNET_asprintf (&ee->write_buf, + "%s\n", + js); + free (js); ee->write_size = strlen (ee->write_buf); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Passing %llu bytes to JSON conversion tool\n", - (unsigned long long) ee->write_size); if (NULL == sr->write_task) sr->write_task = GNUNET_SCHEDULER_add_write_file (GNUNET_TIME_UNIT_FOREVER_REL, diff --git a/src/kyclogic/taler-exchange-helper-measure-test-form b/src/kyclogic/taler-exchange-helper-measure-test-form @@ -34,9 +34,9 @@ do case "$OPTION" in a) # This AML program expects as inputs a full_name - # and a birthdate. - echo "full_name" - echo "birthdate" + # and a date of birth. + echo "FULL_NAME" + echo "DATE_OF_BIRTH" exit 0 ;; c) @@ -90,8 +90,8 @@ J=$(echo "$A" | jq -r 'def get($k): then .[$k] else error("attribute missing") end; - {"full_name":get("full_name"), - "birthdate":get("birthdate")}') + {"FULL_NAME":get("FULL_NAME"), + "DATE_OF_BIRTH":get("DATE_OF_BIRTH")}') # Here we could use those values... echo "$J" >> /dev/null diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am @@ -185,9 +185,12 @@ endif # test_exchange_api_revocation_cs # test_exchange_api_revocation_rsa +check_SCRIPTS = \ + test-sanctions.sh TESTS = \ - $(check_PROGRAMS) + $(check_PROGRAMS) \ + $(check_SCRIPTS) test_auditor_api_cs_SOURCES = \ test_auditor_api.c @@ -559,9 +562,11 @@ test_kyc_api_LDADD = \ EXTRA_DIST = \ $(bin_SCRIPTS) \ + $(check_SCRIPTS) \ valgrind.h \ coins-cs.conf \ coins-rsa.conf \ + sanction-list.json \ test_exchange_api_home/.local/share/taler-auditor/offline-keys/auditor.priv \ test_exchange_api_home/.local/share/taler-exchange/offline/master.priv \ test_auditor_api-cs.conf \ diff --git a/src/testing/sanction-list.json b/src/testing/sanction-list.json @@ -0,0 +1,3 @@ +[{"ssid":"1","FULL_NAME":["Bob"],"DATE_OF_BIRTH":["6.7.1980"]}, + {"ssid":"2","FULL_NAME":["Alice"],"DATE_OF_BIRTH":["5.7.1980"]}, + {"ssid":"3","FULL_NAME":["Carol"],"DATE_OF_BIRTH":["5.7.1980"]}] diff --git a/src/testing/test-sanctions.sh b/src/testing/test-sanctions.sh @@ -32,6 +32,10 @@ function my_cleanup() fi } +echo -n "Testing for robocop" +robocop -h > /dev/null || exit_skip " robocop required" +echo " FOUND" + . setup.sh @@ -67,7 +71,7 @@ ID=$(jq -r .requirements[0].id < "$LAST_RESPONSE") echo -n "Submitting KYC form..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ "http://localhost:8081/kyc-upload/$ID" \ - -d '{"full_name":"Bob","birthdate":"5.7.1980"}' \ + -d '{"FULL_NAME":"Bob","DATE_OF_BIRTH":"5.7.1980"}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") echo $STATUS @@ -77,30 +81,22 @@ then exit_fail "Expected 204, KYC data submitted. got: $STATUS" fi +taler-exchange-sanctionscheck \ + -L INFO \ + -c test_sanctions.conf.edited \ + --reset \ + --test \ + robocop sanction-list.json +PROP=$(echo 'SELECT jproperties FROM exchange.legitimization_outcomes WHERE is_active;' | psql talercheck -Aqt) -bash - -# => begin KYC process -# => submit KYC data -# => run sanction list tool! - +MATCH=$(echo "$PROP" | jq -r .SANCTION_LIST_BEST_MATCH) +if [ "$MATCH" != "1" ] +then + exit_fail "Sanction checker failed to find Bob" +fi echo "Test PASSED" exit 0 - - -echo -n "Creating order to test auth is ok..." >&2 -STATUS=$(curl -H "Content-Type: application/json" -X POST \ - 'http://localhost:9966/private/orders' \ - -H 'Authorization: Bearer '"$NEW_SECRET" \ - -d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"}}' \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") - -if [ "$STATUS" != "200" ] -then - cat "$LAST_RESPONSE" >&2 - exit_fail "Expected 200, order created. got: $STATUS" -fi diff --git a/src/testing/test_kyc_api.c b/src/testing/test_kyc_api.c @@ -771,7 +771,7 @@ run (void *cls, "get-kyc-info-form", 0, /* requirement index */ "application/json", - "{\"form_id\":\"test\",\"full_name\":\"Bob\",\"birthdate\":\"1990-00-00\"}", + "{\"form_id\":\"test\",\"FULL_NAME\":\"Bob\",\"DATE_OF_BIRTH\":\"1990-00-00\"}", MHD_HTTP_NO_CONTENT), /* now this should be allowed */ TALER_TESTING_cmd_wallet_kyc_get ( diff --git a/src/testing/test_sanctions.conf b/src/testing/test_sanctions.conf @@ -40,7 +40,7 @@ FALLBACK = manual-freeze # This check runs on oauth2 FORM_NAME = full_name_and_birthdate # Outputs from this check -OUTPUTS = full_name birthdate +OUTPUTS = FULL_NAME DATE_OF_BIRTH # This is the "default" setting for an account if # it has not yet triggered anything.