diff options
| author | Christian Grothoff <christian@grothoff.org> | 2022-08-11 23:35:33 +0200 | 
|---|---|---|
| committer | Christian Grothoff <christian@grothoff.org> | 2022-08-11 23:35:33 +0200 | 
| commit | 1009084e94b8e8cf19e3b5568c3cccaba2bd2209 (patch) | |
| tree | a346997dedd05f685ba7addc59e288dfa550ad0e /src/exchange | |
| parent | b061ea85c84facfc78c34edface367c5f040bc9c (diff) | |
major rework of the KYC logic, making it more configurable, not complete, but tests pass again
Diffstat (limited to 'src/exchange')
| -rw-r--r-- | src/exchange/Makefile.am | 1 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-aggregator.c | 779 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-httpd.c | 15 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-httpd_batch-withdraw.c | 146 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-httpd_deposit.c | 6 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-httpd_deposits_get.c | 12 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-httpd_keys.c | 45 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-httpd_kyc-check.c | 355 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-httpd_kyc-proof.c | 671 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-httpd_kyc-wallet.c | 119 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-httpd_purses_merge.c | 16 | ||||
| -rw-r--r-- | src/exchange/taler-exchange-httpd_withdraw.c | 170 | 
12 files changed, 1344 insertions, 991 deletions
| diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am index 64db3899..881a3f36 100644 --- a/src/exchange/Makefile.am +++ b/src/exchange/Makefile.am @@ -30,6 +30,7 @@ taler_exchange_aggregator_SOURCES = \    taler-exchange-aggregator.c  taler_exchange_aggregator_LDADD = \    $(LIBGCRYPT_LIBS) \ +  $(top_builddir)/src/kyclogic/libtalerkyclogic.la \    $(top_builddir)/src/json/libtalerjson.la \    $(top_builddir)/src/util/libtalerutil.la \    $(top_builddir)/src/bank-lib/libtalerbank.la \ diff --git a/src/exchange/taler-exchange-aggregator.c b/src/exchange/taler-exchange-aggregator.c index 3d30ccd0..2c279535 100644 --- a/src/exchange/taler-exchange-aggregator.c +++ b/src/exchange/taler-exchange-aggregator.c @@ -1,6 +1,6 @@  /*    This file is part of TALER -  Copyright (C) 2016-2021 Taler Systems SA +  Copyright (C) 2016-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 @@ -26,6 +26,7 @@  #include "taler_exchangedb_lib.h"  #include "taler_exchangedb_plugin.h"  #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h"  #include "taler_bank_service.h" @@ -43,6 +44,12 @@ struct AggregationUnit    struct TALER_MerchantPublicKeyP merchant_pub;    /** +   * Transient amount already found aggregated, +   * set only if @e have_transient is true. +   */ +  struct TALER_Amount trans; + +  /**     * Total amount to be transferred, before subtraction of @e fees.wire and rounding down.     */    struct TALER_Amount total_amount; @@ -84,6 +91,19 @@ struct AggregationUnit     */    const struct TALER_EXCHANGEDB_AccountInfo *wa; +  /** +   * Set to #GNUNET_OK during transient checking +   * while everything is OK. Otherwise see return +   * value of #do_aggregate(). +   */ +  enum GNUNET_GenericReturnValue ret; + +  /** +   * Do we have an entry in the transient table for +   * this aggregation? +   */ +  bool have_transient; +  }; @@ -151,7 +171,6 @@ static struct TALER_EXCHANGEDB_Plugin *db_plugin;   */  static struct GNUNET_SCHEDULER_Task *task; -  /**   * How long should we sleep when idle before trying to find more work?   */ @@ -186,12 +205,12 @@ run_aggregation (void *cls);  /** - * Select a shard to work on. + * Work on transactions unlocked by KYC.   *   * @param cls NULL   */  static void -run_shard (void *cls); +drain_kyc_alerts (void *cls);  /** @@ -226,6 +245,7 @@ shutdown_task (void *cls)      GNUNET_SCHEDULER_cancel (task);      task = NULL;    } +  TALER_KYCLOGIC_kyc_done ();    TALER_EXCHANGEDB_plugin_unload (db_plugin);    db_plugin = NULL;    TALER_EXCHANGEDB_unload_accounts (); @@ -353,109 +373,170 @@ release_shard (struct Shard *s)  } +/** + * Trigger the wire transfer for the @a au_active + * and delete the record of the aggregation. + * + * @param au_active information about the aggregation + */ +static enum GNUNET_DB_QueryStatus +trigger_wire_transfer (const struct AggregationUnit *au_active) +{ +  enum GNUNET_DB_QueryStatus qs; + +  GNUNET_log (GNUNET_ERROR_TYPE_INFO, +              "Preparing wire transfer of %s to %s\n", +              TALER_amount2s (&au_active->final_amount), +              TALER_B2S (&au_active->merchant_pub)); +  { +    void *buf; +    size_t buf_size; + +    TALER_BANK_prepare_transfer (au_active->payto_uri, +                                 &au_active->final_amount, +                                 exchange_base_url, +                                 &au_active->wtid, +                                 &buf, +                                 &buf_size); +    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, +                "Storing %u bytes of wire prepare data\n", +                (unsigned int) buf_size); +    /* Commit our intention to execute the wire transfer! */ +    qs = db_plugin->wire_prepare_data_insert (db_plugin->cls, +                                              au_active->wa->method, +                                              buf, +                                              buf_size); +    GNUNET_free (buf); +  } +  /* Commit the WTID data to 'wire_out'  */ +  if (qs >= 0) +    qs = db_plugin->store_wire_transfer_out (db_plugin->cls, +                                             au_active->execution_time, +                                             &au_active->wtid, +                                             &au_active->h_payto, +                                             au_active->wa->section_name, +                                             &au_active->final_amount); + +  if ( (qs >= 0) && +       au_active->have_transient) +    qs = db_plugin->delete_aggregation_transient (db_plugin->cls, +                                                  &au_active->h_payto, +                                                  &au_active->wtid); +  return qs; +} + + +/** + * Callback to return all applicable amounts for the KYC + * decision to @ a cb. + * + * @param cls a `struct AggregationUnit *` + * @param limit time limit for the iteration + * @param cb function to call with the amounts + * @param cb_cls closure for @a cb + */  static void -run_aggregation (void *cls) +return_relevant_amounts (void *cls, +                         struct GNUNET_TIME_Absolute limit, +                         TALER_EXCHANGEDB_KycAmountCallback cb, +                         void *cb_cls)  { -  struct Shard *s = cls; -  struct AggregationUnit au_active; +  const struct AggregationUnit *au_active = cls;    enum GNUNET_DB_QueryStatus qs; -  struct TALER_Amount trans; -  bool have_transient = true; /* squash compiler warning */ -  task = NULL;    GNUNET_log (GNUNET_ERROR_TYPE_INFO, -              "Checking for ready deposits to aggregate\n"); -  /* make sure we have current fees */ -  memset (&au_active, -          0, -          sizeof (au_active)); -  au_active.execution_time = GNUNET_TIME_timestamp_get (); +              "Returning amount %s in KYC check\n", +              TALER_amount2s (&au_active->total_amount));    if (GNUNET_OK != -      db_plugin->start_deferred_wire_out (db_plugin->cls)) +      cb (cb_cls, +          &au_active->total_amount, +          GNUNET_TIME_absolute_get ())) +    return; +  qs = db_plugin->select_aggregation_amounts_for_kyc_check ( +    db_plugin->cls, +    &au_active->h_payto, +    limit, +    cb, +    cb_cls); +  if (GNUNET_DB_STATUS_HARD_ERROR == qs)    {      GNUNET_log (GNUNET_ERROR_TYPE_ERROR, -                "Failed to start database transaction!\n"); -    global_ret = EXIT_FAILURE; -    GNUNET_SCHEDULER_shutdown (); -    release_shard (s); -    return; +                "Failed to select aggregation amounts for KYC limit check!\n");    } -  qs = db_plugin->get_ready_deposit ( +} + + +/** + * Test if KYC is required for a transfer to @a h_payto. + * + * @param au_active aggregation unit to check for + * @return true if KYC checks are satisfied + */ +static bool +kyc_satisfied (const struct AggregationUnit *au_active) +{ +  const char *requirement; +  uint64_t legi_row; +  enum GNUNET_DB_QueryStatus qs; + +  requirement = TALER_KYCLOGIC_kyc_test_required ( +    TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT, +    &au_active->h_payto, +    db_plugin->select_satisfied_kyc_processes,      db_plugin->cls, -    s->shard_start, -    s->shard_end, -    kyc_off ? true : false, -    &au_active.merchant_pub, -    &au_active.payto_uri); -  switch (qs) +    &return_relevant_amounts, +    (void *) au_active); +  GNUNET_log (GNUNET_ERROR_TYPE_INFO, +              "KYC requirement for %s is %s\n", +              TALER_amount2s (&au_active->total_amount), +              requirement); +  if (NULL == requirement) +    return true; +  qs = db_plugin->insert_kyc_requirement_for_account ( +    db_plugin->cls, +    requirement, +    &au_active->h_payto, +    &legi_row); +  if (qs < 0)    { -  case GNUNET_DB_STATUS_HARD_ERROR: -    cleanup_au (&au_active); -    db_plugin->rollback (db_plugin->cls);      GNUNET_log (GNUNET_ERROR_TYPE_ERROR, -                "Failed to begin deposit iteration!\n"); -    global_ret = EXIT_FAILURE; -    GNUNET_SCHEDULER_shutdown (); -    release_shard (s); -    return; -  case GNUNET_DB_STATUS_SOFT_ERROR: -    cleanup_au (&au_active); -    db_plugin->rollback (db_plugin->cls); -    GNUNET_assert (NULL == task); -    task = GNUNET_SCHEDULER_add_now (&run_aggregation, -                                     s); -    return; -  case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: -    { -      uint64_t counter = s->work_counter; -      struct GNUNET_TIME_Relative duration -        = GNUNET_TIME_absolute_get_duration (s->start_time.abs_time); - -      cleanup_au (&au_active); -      db_plugin->rollback (db_plugin->cls); -      GNUNET_log (GNUNET_ERROR_TYPE_INFO, -                  "Completed shard [%u,%u] after %s with %llu deposits\n", -                  (unsigned int) s->shard_start, -                  (unsigned int) s->shard_end, -                  GNUNET_TIME_relative2s (duration, -                                          true), -                  (unsigned long long) counter); -      release_shard (s); -      if ( (GNUNET_YES == test_mode) && -           (0 == counter) ) -      { -        /* in test mode, shutdown after a shard is done with 0 work */ -        GNUNET_SCHEDULER_shutdown (); -        return; -      } -      GNUNET_assert (NULL == task); -      /* If we ended up doing zero work, sleep a bit */ -      if (0 == counter) -        task = GNUNET_SCHEDULER_add_delayed (aggregator_idle_sleep_interval, -                                             &run_shard, -                                             NULL); -      else -        task = GNUNET_SCHEDULER_add_now (&run_shard, -                                         NULL); -      return; -    } -  case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: -    s->work_counter++; -    /* continued below */ -    break; +                "Failed to persist KYC requirement `%s' in DB!\n", +                requirement); +  } +  else +  { +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "New legitimization process %llu started\n", +                (unsigned long long) legi_row);    } -  au_active.wa = TALER_EXCHANGEDB_find_account_by_payto_uri ( -    au_active.payto_uri); -  if (NULL == au_active.wa) +  return false; +} + + +/** + * Perform the main aggregation work for @a au.  Expects to be in + * a working transaction, which the caller must also ultimately commit + * (or rollback) depending on our return value. + * + * @param[in,out] au aggregation unit to work on + * @return #GNUNET_OK if aggregation succeeded, + *         #GNUNET_NO to rollback and try again (serialization issue) + *         #GNUNET_SYSERR hard error, terminate aggregator process + */ +static enum GNUNET_GenericReturnValue +do_aggregate (struct AggregationUnit *au) +{ +  enum GNUNET_DB_QueryStatus qs; + +  au->wa = TALER_EXCHANGEDB_find_account_by_payto_uri ( +    au->payto_uri); +  if (NULL == au->wa)    {      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,                  "No exchange account configured for `%s', please fix your setup to continue!\n", -                au_active.payto_uri); +                au->payto_uri);      global_ret = EXIT_FAILURE; -    GNUNET_SCHEDULER_shutdown (); -    db_plugin->rollback (db_plugin->cls); -    release_shard (s); -    return; +    return GNUNET_SYSERR;    }    { @@ -464,234 +545,265 @@ run_aggregation (void *cls)      struct TALER_MasterSignatureP master_sig;      qs = db_plugin->get_wire_fee (db_plugin->cls, -                                  au_active.wa->method, -                                  au_active.execution_time, +                                  au->wa->method, +                                  au->execution_time,                                    &start_date,                                    &end_date, -                                  &au_active.fees, +                                  &au->fees,                                    &master_sig);      if (0 >= qs)      {        GNUNET_log (GNUNET_ERROR_TYPE_ERROR,                    "Could not get wire fees for %s at %s. Aborting run.\n", -                  au_active.wa->method, -                  GNUNET_TIME_timestamp2s (au_active.execution_time)); +                  au->wa->method, +                  GNUNET_TIME_timestamp2s (au->execution_time));        global_ret = EXIT_FAILURE; -      GNUNET_SCHEDULER_shutdown (); -      db_plugin->rollback (db_plugin->cls); -      release_shard (s); -      return; +      return GNUNET_SYSERR;      }    } -    /* Now try to find other deposits to aggregate */    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,                "Found ready deposit for %s, aggregating by target %s\n", -              TALER_B2S (&au_active.merchant_pub), -              au_active.payto_uri); -  TALER_payto_hash (au_active.payto_uri, -                    &au_active.h_payto); - +              TALER_B2S (&au->merchant_pub), +              au->payto_uri);    qs = db_plugin->select_aggregation_transient (db_plugin->cls, -                                                &au_active.h_payto, -                                                au_active.wa->section_name, -                                                &au_active.wtid, -                                                &trans); +                                                &au->h_payto, +                                                &au->merchant_pub, +                                                au->wa->section_name, +                                                &au->wtid, +                                                &au->trans);    switch (qs)    {    case GNUNET_DB_STATUS_HARD_ERROR:      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,                  "Failed to lookup transient aggregates!\n"); -    cleanup_au (&au_active); -    db_plugin->rollback (db_plugin->cls);      global_ret = EXIT_FAILURE; -    GNUNET_SCHEDULER_shutdown (); -    release_shard (s); -    return; +    return GNUNET_SYSERR;    case GNUNET_DB_STATUS_SOFT_ERROR:      /* serializiability issue, try again */      GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,                  "Serialization issue, trying again later!\n"); -    db_plugin->rollback (db_plugin->cls); -    cleanup_au (&au_active); -    GNUNET_assert (NULL == task); -    task = GNUNET_SCHEDULER_add_now (&run_aggregation, -                                     s); -    return; +    return GNUNET_NO;    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:      GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, -                                &au_active.wtid, -                                sizeof (au_active.wtid)); -    have_transient = false; +                                &au->wtid, +                                sizeof (au->wtid)); +    au->have_transient = false;      break;    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: -    have_transient = true; +    au->have_transient = true;      break;    }    qs = db_plugin->aggregate (db_plugin->cls, -                             &au_active.h_payto, -                             &au_active.merchant_pub, -                             &au_active.wtid, -                             &au_active.total_amount); +                             &au->h_payto, +                             &au->merchant_pub, +                             &au->wtid, +                             &au->total_amount);    if (GNUNET_DB_STATUS_HARD_ERROR == qs)    {      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,                  "Failed to execute aggregation!\n"); -    cleanup_au (&au_active); -    db_plugin->rollback (db_plugin->cls);      global_ret = EXIT_FAILURE; -    GNUNET_SCHEDULER_shutdown (); -    release_shard (s); -    return; +    return GNUNET_SYSERR;    }    if (GNUNET_DB_STATUS_SOFT_ERROR == qs)    {      /* serializiability issue, try again */      GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,                  "Serialization issue, trying again later!\n"); -    db_plugin->rollback (db_plugin->cls); -    cleanup_au (&au_active); -    GNUNET_assert (NULL == task); -    task = GNUNET_SCHEDULER_add_now (&run_aggregation, -                                     s); -    return; +    return GNUNET_NO;    }    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,                "Aggregation total is %s.\n", -              TALER_amount2s (&au_active.total_amount)); +              TALER_amount2s (&au->total_amount));    /* Subtract wire transfer fee and round to the unit supported by the       wire transfer method; Check if after rounding down, we still have       an amount to transfer, and if not mark as 'tiny'. */ -  if (have_transient) +  if (au->have_transient)      GNUNET_assert (0 <= -                   TALER_amount_add (&au_active.total_amount, -                                     &au_active.total_amount, -                                     &trans)); +                   TALER_amount_add (&au->total_amount, +                                     &au->total_amount, +                                     &au->trans)); + +    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,                "Rounding aggregate of %s\n", -              TALER_amount2s (&au_active.total_amount)); +              TALER_amount2s (&au->total_amount));    if ( (0 >= -        TALER_amount_subtract (&au_active.final_amount, -                               &au_active.total_amount, -                               &au_active.fees.wire)) || +        TALER_amount_subtract (&au->final_amount, +                               &au->total_amount, +                               &au->fees.wire)) ||         (GNUNET_SYSERR == -        TALER_amount_round_down (&au_active.final_amount, +        TALER_amount_round_down (&au->final_amount,                                   ¤cy_round_unit)) || -       (TALER_amount_is_zero (&au_active.final_amount)) ) +       (TALER_amount_is_zero (&au->final_amount)) || +       (! kyc_satisfied (au)) )    {      GNUNET_log (GNUNET_ERROR_TYPE_INFO, -                "Aggregate value too low for transfer (%d/%s)\n", +                "Not ready for wire transfer (%d/%s)\n",                  qs, -                TALER_amount2s (&au_active.final_amount)); -    if (have_transient) +                TALER_amount2s (&au->final_amount)); +    if (au->have_transient)        qs = db_plugin->update_aggregation_transient (db_plugin->cls, -                                                    &au_active.h_payto, -                                                    &au_active.wtid, -                                                    &au_active.total_amount); +                                                    &au->h_payto, +                                                    &au->wtid, +                                                    &au->total_amount);      else        qs = db_plugin->create_aggregation_transient (db_plugin->cls, -                                                    &au_active.h_payto, -                                                    au_active.wa->section_name, -                                                    &au_active.wtid, -                                                    &au_active.total_amount); +                                                    &au->h_payto, +                                                    au->wa->section_name, +                                                    &au->merchant_pub, +                                                    &au->wtid, +                                                    &au->total_amount);      if (GNUNET_DB_STATUS_SOFT_ERROR == qs)      {        GNUNET_log (GNUNET_ERROR_TYPE_INFO,                    "Serialization issue, trying again later!\n"); -      db_plugin->rollback (db_plugin->cls); -      cleanup_au (&au_active); -      /* start again */ -      GNUNET_assert (NULL == task); -      task = GNUNET_SCHEDULER_add_now (&run_aggregation, -                                       s); -      return; +      return GNUNET_NO;      }      if (GNUNET_DB_STATUS_HARD_ERROR == qs)      {        GNUNET_break (0); -      db_plugin->rollback (db_plugin->cls); -      cleanup_au (&au_active);        global_ret = EXIT_FAILURE; -      GNUNET_SCHEDULER_shutdown (); -      release_shard (s); -      return; +      return GNUNET_SYSERR;      }      /* commit */ -    (void) commit_or_warn (); -    cleanup_au (&au_active); - -    /* start again */ -    GNUNET_assert (NULL == task); -    task = GNUNET_SCHEDULER_add_now (&run_aggregation, -                                     s); -    return; +    return GNUNET_OK;    } -  GNUNET_log (GNUNET_ERROR_TYPE_INFO, -              "Preparing wire transfer of %s to %s\n", -              TALER_amount2s (&au_active.final_amount), -              TALER_B2S (&au_active.merchant_pub)); -  { -    void *buf; -    size_t buf_size; -    TALER_BANK_prepare_transfer (au_active.payto_uri, -                                 &au_active.final_amount, -                                 exchange_base_url, -                                 &au_active.wtid, -                                 &buf, -                                 &buf_size); -    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, -                "Storing %u bytes of wire prepare data\n", -                (unsigned int) buf_size); -    /* Commit our intention to execute the wire transfer! */ -    qs = db_plugin->wire_prepare_data_insert (db_plugin->cls, -                                              au_active.wa->method, -                                              buf, -                                              buf_size); -    GNUNET_free (buf); +  qs = trigger_wire_transfer (au); +  switch (qs) +  { +  case GNUNET_DB_STATUS_SOFT_ERROR: +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "Serialization issue during aggregation; trying again later!\n"); +    return GNUNET_NO; +  case GNUNET_DB_STATUS_HARD_ERROR: +    GNUNET_break (0); +    global_ret = EXIT_FAILURE; +    return GNUNET_SYSERR; +  default: +    return GNUNET_OK;    } -  /* Commit the WTID data to 'wire_out'  */ -  if (qs >= 0) -    qs = db_plugin->store_wire_transfer_out (db_plugin->cls, -                                             au_active.execution_time, -                                             &au_active.wtid, -                                             &au_active.h_payto, -                                             au_active.wa->section_name, -                                             &au_active.final_amount); +} -  if ( (qs >= 0) && -       have_transient) -    qs = db_plugin->delete_aggregation_transient (db_plugin->cls, -                                                  &au_active.h_payto, -                                                  &au_active.wtid); -  cleanup_au (&au_active); -  if (GNUNET_DB_STATUS_SOFT_ERROR == qs) +static void +run_aggregation (void *cls) +{ +  struct Shard *s = cls; +  struct AggregationUnit au_active; +  enum GNUNET_DB_QueryStatus qs; +  enum GNUNET_GenericReturnValue ret; + +  task = NULL; +  GNUNET_log (GNUNET_ERROR_TYPE_INFO, +              "Checking for ready deposits to aggregate\n"); +  /* make sure we have current fees */ +  memset (&au_active, +          0, +          sizeof (au_active)); +  au_active.execution_time = GNUNET_TIME_timestamp_get (); +  if (GNUNET_OK != +      db_plugin->start_deferred_wire_out (db_plugin->cls))    { -    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, -                "Serialization issue for prepared wire data; trying again later!\n"); +    GNUNET_log (GNUNET_ERROR_TYPE_ERROR, +                "Failed to start database transaction!\n"); +    global_ret = EXIT_FAILURE; +    GNUNET_SCHEDULER_shutdown (); +    release_shard (s); +    return; +  } +  qs = db_plugin->get_ready_deposit ( +    db_plugin->cls, +    s->shard_start, +    s->shard_end, +    &au_active.merchant_pub, +    &au_active.payto_uri); +  switch (qs) +  { +  case GNUNET_DB_STATUS_HARD_ERROR: +    cleanup_au (&au_active); +    db_plugin->rollback (db_plugin->cls); +    GNUNET_log (GNUNET_ERROR_TYPE_ERROR, +                "Failed to begin deposit iteration!\n"); +    global_ret = EXIT_FAILURE; +    GNUNET_SCHEDULER_shutdown (); +    release_shard (s); +    return; +  case GNUNET_DB_STATUS_SOFT_ERROR: +    cleanup_au (&au_active);      db_plugin->rollback (db_plugin->cls); -    /* start again */      GNUNET_assert (NULL == task);      task = GNUNET_SCHEDULER_add_now (&run_aggregation,                                       s);      return; +  case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: +    { +      uint64_t counter = s->work_counter; +      struct GNUNET_TIME_Relative duration +        = GNUNET_TIME_absolute_get_duration (s->start_time.abs_time); + +      cleanup_au (&au_active); +      db_plugin->rollback (db_plugin->cls); +      GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                  "Completed shard [%u,%u] after %s with %llu deposits\n", +                  (unsigned int) s->shard_start, +                  (unsigned int) s->shard_end, +                  GNUNET_TIME_relative2s (duration, +                                          true), +                  (unsigned long long) counter); +      release_shard (s); +      if ( (GNUNET_YES == test_mode) && +           (0 == counter) ) +      { +        /* in test mode, shutdown after a shard is done with 0 work */ +        GNUNET_SCHEDULER_shutdown (); +        return; +      } +      GNUNET_assert (NULL == task); +      /* If we ended up doing zero work, sleep a bit */ +      if (0 == counter) +        task = GNUNET_SCHEDULER_add_delayed (aggregator_idle_sleep_interval, +                                             &drain_kyc_alerts, +                                             NULL); +      else +        task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, +                                         NULL); +      return; +    } +  case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: +    s->work_counter++; +    /* continued below */ +    break;    } -  if (GNUNET_DB_STATUS_HARD_ERROR == qs) + +  TALER_payto_hash (au_active.payto_uri, +                    &au_active.h_payto); +  ret = do_aggregate (&au_active); +  cleanup_au (&au_active); +  switch (ret)    { -    GNUNET_break (0); -    db_plugin->rollback (db_plugin->cls); -    /* die hard */ -    global_ret = EXIT_FAILURE; +  case GNUNET_SYSERR:      GNUNET_SCHEDULER_shutdown (); +    db_plugin->rollback (db_plugin->cls);      release_shard (s);      return; +  case GNUNET_NO: +    db_plugin->rollback (db_plugin->cls); +    GNUNET_assert (NULL == task); +    task = GNUNET_SCHEDULER_add_now (&run_aggregation, +                                     s); +    return; +  case GNUNET_OK: +    /* continued below */ +    break;    } -  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, -              "Stored wire transfer out instructions\n"); +  GNUNET_log (GNUNET_ERROR_TYPE_INFO, +              "Committing aggregation result\n");    /* Now we can finally commit the overall transaction, as we are       again consistent if all of this passes. */ @@ -699,8 +811,8 @@ run_aggregation (void *cls)    {    case GNUNET_DB_STATUS_SOFT_ERROR:      /* try again */ -    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, -                "Commit issue for prepared wire data; trying again later!\n"); +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "Serialization issue on commit; trying again later!\n");      GNUNET_assert (NULL == task);      task = GNUNET_SCHEDULER_add_now (&run_aggregation,                                       s); @@ -714,7 +826,7 @@ run_aggregation (void *cls)      return;    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:      GNUNET_log (GNUNET_ERROR_TYPE_INFO, -                "Preparation complete, going again\n"); +                "Commit complete, going again\n");      GNUNET_assert (NULL == task);      task = GNUNET_SCHEDULER_add_now (&run_aggregation,                                       s); @@ -792,6 +904,197 @@ run_shard (void *cls)  /** + * Function called on transient aggregations matching + * a particular hash of a payto URI. + * + * @param cls + * @param payto_uri corresponding payto URI + * @param wtid wire transfer identifier of transient aggregation + * @param merchant_pub public key of the merchant + * @param total amount aggregated so far + * @return true to continue to iterate + */ +static bool +handle_transient_cb ( +  void *cls, +  const char *payto_uri, +  const struct TALER_WireTransferIdentifierRawP *wtid, +  const struct TALER_MerchantPublicKeyP *merchant_pub, +  const struct TALER_Amount *total) +{ +  struct AggregationUnit *au = cls; + +  if (GNUNET_OK != au->ret) +  { +    GNUNET_break (0); +    return false; +  } +  au->payto_uri = GNUNET_strdup (payto_uri); +  au->wtid = *wtid; +  au->merchant_pub = *merchant_pub; +  au->trans = *total; +  au->have_transient = true; +  au->ret = do_aggregate (au); +  GNUNET_free (au->payto_uri); +  return (GNUNET_OK == au->ret); +} + + +static void +drain_kyc_alerts (void *cls) +{ +  enum GNUNET_DB_QueryStatus qs; +  struct AggregationUnit au; + +  (void) cls; +  task = NULL; +  memset (&au, +          0, +          sizeof (au)); +  au.execution_time = GNUNET_TIME_timestamp_get (); +  if (GNUNET_SYSERR == +      db_plugin->preflight (db_plugin->cls)) +  { +    GNUNET_log (GNUNET_ERROR_TYPE_ERROR, +                "Failed to obtain database connection!\n"); +    global_ret = EXIT_FAILURE; +    GNUNET_SCHEDULER_shutdown (); +    return; +  } +  if (GNUNET_OK != +      db_plugin->start (db_plugin->cls, +                        "handle kyc alerts")) +  { +    GNUNET_log (GNUNET_ERROR_TYPE_ERROR, +                "Failed to start database transaction!\n"); +    global_ret = EXIT_FAILURE; +    GNUNET_SCHEDULER_shutdown (); +    return; +  } +  while (1) +  { +    qs = db_plugin->drain_kyc_alert (db_plugin->cls, +                                     1, +                                     &au.h_payto); +    switch (qs) +    { +    case GNUNET_DB_STATUS_HARD_ERROR: +      GNUNET_break (0); +      db_plugin->rollback (db_plugin->cls); +      GNUNET_assert (NULL == task); +      task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, +                                       NULL); +      return; +    case GNUNET_DB_STATUS_SOFT_ERROR: +      db_plugin->rollback (db_plugin->cls); +      GNUNET_assert (NULL == task); +      task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, +                                       NULL); +      return; +    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: +      qs = db_plugin->commit (db_plugin->cls); +      if (qs < 0) +        GNUNET_log (GNUNET_ERROR_TYPE_WARNING, +                    "Failed to commit KYC drain\n"); +      GNUNET_assert (NULL == task); +      task = GNUNET_SCHEDULER_add_now (&run_shard, +                                       NULL); +      return; +    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: +      /* handled below */ +      break; +    } + +    au.ret = GNUNET_OK; +    qs = db_plugin->find_aggregation_transient (db_plugin->cls, +                                                &au.h_payto, +                                                &handle_transient_cb, +                                                &au); +    switch (qs) +    { +    case GNUNET_DB_STATUS_HARD_ERROR: +      GNUNET_log (GNUNET_ERROR_TYPE_ERROR, +                  "Failed to lookup transient aggregates!\n"); +      db_plugin->rollback (db_plugin->cls); +      GNUNET_assert (NULL == task); +      task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, +                                       NULL); +      return; +    case GNUNET_DB_STATUS_SOFT_ERROR: +      /* serializiability issue, try again */ +      GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, +                  "Serialization issue, trying again later!\n"); +      db_plugin->rollback (db_plugin->cls); +      GNUNET_assert (NULL == task); +      task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, +                                       NULL); +      return; +    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: +      continue; /* while (1) */ +    default: +      break; +    } +    break; +  } /* while(1) */ + +  { +    enum GNUNET_GenericReturnValue ret; + +    ret = au.ret; +    cleanup_au (&au); +    switch (ret) +    { +    case GNUNET_SYSERR: +      GNUNET_break (0); +      GNUNET_SCHEDULER_shutdown (); +      db_plugin->rollback (db_plugin->cls); /* just in case */ +      return; +    case GNUNET_NO: +      db_plugin->rollback (db_plugin->cls); +      GNUNET_assert (NULL == task); +      task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, +                                       NULL); +      return; +    case GNUNET_OK: +      /* continued below */ +      break; +    } +  } + +  switch (commit_or_warn ()) +  { +  case GNUNET_DB_STATUS_SOFT_ERROR: +    /* try again */ +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "Serialization issue on commit; trying again later!\n"); +    GNUNET_assert (NULL == task); +    task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, +                                     NULL); +    return; +  case GNUNET_DB_STATUS_HARD_ERROR: +    GNUNET_break (0); +    global_ret = EXIT_FAILURE; +    GNUNET_SCHEDULER_shutdown (); +    db_plugin->rollback (db_plugin->cls); /* just in case */ +    return; +  case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "Commit complete, going again\n"); +    GNUNET_assert (NULL == task); +    task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts, +                                     NULL); +    return; +  default: +    GNUNET_break (0); +    global_ret = EXIT_FAILURE; +    GNUNET_SCHEDULER_shutdown (); +    db_plugin->rollback (db_plugin->cls); /* just in case */ +    return; +  } +} + + +/**   * First task.   *   * @param cls closure, NULL @@ -811,7 +1114,8 @@ run (void *cls,    (void) cfgfile;    cfg = c; -  if (GNUNET_OK != parse_aggregator_config ()) +  if (GNUNET_OK != +      parse_aggregator_config ())    {      cfg = NULL;      global_ret = EXIT_NOTCONFIGURED; @@ -832,11 +1136,18 @@ run (void *cls,      shard_size = 1U + INT32_MAX;    else      shard_size = (uint32_t) ass; +  if (GNUNET_OK != +      TALER_KYCLOGIC_kyc_init (cfg)) +  { +    cfg = NULL; +    global_ret = EXIT_NOTCONFIGURED; +    return; +  } +  GNUNET_SCHEDULER_add_shutdown (&shutdown_task, +                                 NULL);    GNUNET_assert (NULL == task); -  task = GNUNET_SCHEDULER_add_now (&run_shard, +  task = GNUNET_SCHEDULER_add_now (&drain_kyc_alerts,                                     NULL); -  GNUNET_SCHEDULER_add_shutdown (&shutdown_task, -                                 cls);  } diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c index fefded57..e7d07043 100644 --- a/src/exchange/taler-exchange-httpd.c +++ b/src/exchange/taler-exchange-httpd.c @@ -27,6 +27,7 @@  #include <sched.h>  #include <sys/resource.h>  #include <limits.h> +#include "taler_kyclogic_lib.h"  #include "taler_mhd_lib.h"  #include "taler-exchange-httpd_auditors.h"  #include "taler-exchange-httpd_batch-deposit.h" @@ -1215,13 +1216,14 @@ handle_mhd_request (void *cls,        .url = "kyc-check",        .method = MHD_HTTP_METHOD_GET,        .handler.get = &TEH_handler_kyc_check, -      .nargs = 1 +      .nargs = 2      },      {        .url = "kyc-proof",        .method = MHD_HTTP_METHOD_GET,        .handler.get = &TEH_handler_kyc_proof, -      .nargs = 1 +      .nargs = 128, +      .nargs_is_upper_bound = true      },      {        .url = "kyc-wallet", @@ -1680,6 +1682,11 @@ parse_kyc_oauth_cfg (void)  static enum GNUNET_GenericReturnValue  exchange_serve_process_config (void)  { +  if (GNUNET_OK != +      TALER_KYCLOGIC_kyc_init (TEH_cfg)) +  { +    return GNUNET_SYSERR; +  }    {      char *kyc_mode; @@ -2094,8 +2101,12 @@ do_shutdown (void *cls)    TEH_purses_get_cleanup ();    TEH_kyc_check_cleanup ();    TEH_kyc_proof_cleanup (); +  TALER_KYCLOGIC_kyc_done ();    if (NULL != mhd) +  {      MHD_stop_daemon (mhd); +    mhd = NULL; +  }    TEH_wire_done ();    TEH_extensions_done ();    TEH_keys_finished (); diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.c b/src/exchange/taler-exchange-httpd_batch-withdraw.c index d306838b..9e24ff4c 100644 --- a/src/exchange/taler-exchange-httpd_batch-withdraw.c +++ b/src/exchange/taler-exchange-httpd_batch-withdraw.c @@ -27,6 +27,7 @@  #include <gnunet/gnunet_util_lib.h>  #include <jansson.h>  #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h"  #include "taler_mhd_lib.h"  #include "taler-exchange-httpd_batch-withdraw.h"  #include "taler-exchange-httpd_responses.h" @@ -87,6 +88,17 @@ struct BatchWithdrawContext    struct PlanchetContext *planchets;    /** +   * Hash of the payto-URI representing the reserve +   * from which we are withdrawing. +   */ +  struct TALER_PaytoHashP h_payto; + +  /** +   * Current time for the DB transaction. +   */ +  struct GNUNET_TIME_Timestamp now; + +  /**     * Total amount from all coins with fees.     */    struct TALER_Amount batch_total; @@ -100,6 +112,45 @@ struct BatchWithdrawContext  /** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Called within a database transaction, so must + * not start a new one. + * + * @param cls closure, identifies the event type and + *        account to iterate over events for + * @param limit maximum time-range for which events + *        should be fetched (timestamp in the past) + * @param cb function to call on each event found, + *        events must be returned in reverse chronological + *        order + * @param cb_cls closure for @a cb + */ +static void +batch_withdraw_amount_cb (void *cls, +                          struct GNUNET_TIME_Absolute limit, +                          TALER_EXCHANGEDB_KycAmountCallback cb, +                          void *cb_cls) +{ +  struct BatchWithdrawContext *wc = cls; +  enum GNUNET_DB_QueryStatus qs; + +  if (GNUNET_OK != +      cb (cb_cls, +          &wc->batch_total, +          wc->now.abs_time)) +    return; +  qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( +    TEH_plugin->cls, +    &wc->h_payto, +    limit, +    cb, +    cb_cls); +  GNUNET_break (qs >= 0); +} + + +/**   * Function implementing withdraw 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, @@ -127,15 +178,46 @@ batch_withdraw_transaction (void *cls,    enum GNUNET_DB_QueryStatus qs;    bool balance_ok = false;    bool found = false; +  const char *kyc_required; -  now = GNUNET_TIME_timestamp_get (); +  wc->now = GNUNET_TIME_timestamp_get (); +  qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls, +                                        wc->reserve_pub, +                                        &wc->h_payto); +  if (qs < 0) +    return qs; +  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) +  { +    *mhd_ret = TALER_MHD_reply_with_error (connection, +                                           MHD_HTTP_NOT_FOUND, +                                           TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, +                                           NULL); +    return GNUNET_DB_STATUS_HARD_ERROR; +  } +  kyc_required = TALER_KYCLOGIC_kyc_test_required ( +    TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, +    &wc->h_payto, +    TEH_plugin->select_satisfied_kyc_processes, +    TEH_plugin->cls, +    &batch_withdraw_amount_cb, +    wc); +  if (NULL != kyc_required) +  { +    /* insert KYC requirement into DB! */ +    wc->kyc.ok = false; +    return TEH_plugin->insert_kyc_requirement_for_account ( +      TEH_plugin->cls, +      kyc_required, +      &wc->h_payto, +      &wc->kyc.payment_target_uuid); +  } +  wc->kyc.ok = true;    qs = TEH_plugin->do_batch_withdraw (TEH_plugin->cls,                                        now,                                        wc->reserve_pub,                                        &wc->batch_total,                                        &found,                                        &balance_ok, -                                      &wc->kyc,                                        &ruuid);    if (0 > qs)    { @@ -164,55 +246,6 @@ batch_withdraw_transaction (void *cls,      return GNUNET_DB_STATUS_HARD_ERROR;    } -  if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && -       (! wc->kyc.ok) && -       (TALER_EXCHANGEDB_KYC_W2W == wc->kyc.type) ) -  { -    /* Wallet-to-wallet payments _always_ require KYC */ -    *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( -      connection, -      MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, -      GNUNET_JSON_pack_uint64 ("payment_target_uuid", -                               wc->kyc.payment_target_uuid)); -    return GNUNET_DB_STATUS_HARD_ERROR; -  } -  if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && -       (! wc->kyc.ok) && -       (TALER_EXCHANGEDB_KYC_WITHDRAW == wc->kyc.type) && -       (! GNUNET_TIME_relative_is_zero (TEH_kyc_config.withdraw_period)) ) -  { -    /* Withdraws require KYC if above threshold */ -    enum GNUNET_DB_QueryStatus qs2; -    bool below_limit; - -    qs2 = TEH_plugin->do_withdraw_limit_check ( -      TEH_plugin->cls, -      ruuid, -      GNUNET_TIME_absolute_subtract (now.abs_time, -                                     TEH_kyc_config.withdraw_period), -      &TEH_kyc_config.withdraw_limit, -      &below_limit); -    if (0 > qs2) -    { -      GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs2); -      if (GNUNET_DB_STATUS_HARD_ERROR == qs2) -        *mhd_ret = TALER_MHD_reply_with_error (connection, -                                               MHD_HTTP_INTERNAL_SERVER_ERROR, -                                               TALER_EC_GENERIC_DB_FETCH_FAILED, -                                               "do_withdraw_limit_check"); -      return qs2; -    } -    if (! below_limit) -    { -      *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( -        connection, -        MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, -        GNUNET_JSON_pack_uint64 ("payment_target_uuid", -                                 wc->kyc.payment_target_uuid)); -      return GNUNET_DB_STATUS_HARD_ERROR; -    } -  } -    /* Add information about each planchet in the batch */    for (unsigned int i = 0; i<wc->planchets_length; i++)    { @@ -291,6 +324,16 @@ generate_reply_success (const struct TEH_RequestContext *rc,  {    json_t *sigs; +  if (! wc->kyc.ok) +  { +    /* KYC required */ +    return TALER_MHD_REPLY_JSON_PACK ( +      rc->connection, +      MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, +      GNUNET_JSON_pack_uint64 ("payment_target_uuid", +                               wc->kyc.payment_target_uuid)); +  } +    sigs = json_array ();    GNUNET_assert (NULL != sigs);    for (unsigned int i = 0; i<wc->planchets_length; i++) @@ -633,7 +676,6 @@ TEH_handler_batch_withdraw (struct TEH_RequestContext *rc,                   TALER_amount_set_zero (TEH_currency,                                          &wc.batch_total));    wc.reserve_pub = reserve_pub; -    {      enum GNUNET_GenericReturnValue res; diff --git a/src/exchange/taler-exchange-httpd_deposit.c b/src/exchange/taler-exchange-httpd_deposit.c index b7466939..0484ab07 100644 --- a/src/exchange/taler-exchange-httpd_deposit.c +++ b/src/exchange/taler-exchange-httpd_deposit.c @@ -184,7 +184,7 @@ deposit_transaction (void *cls,    }    if (in_conflict)    { -    /* FIXME #7267: conficting contract != insufficient funds */ +    /* FIXME #7267: conflicting contract != insufficient funds */      *mhd_ret        = TEH_RESPONSE_reply_coin_insufficient_funds (            connection, @@ -426,7 +426,7 @@ TEH_handler_deposit (struct MHD_Connection *connection,                                     &h_wire,                                     &deposit.h_contract_terms,                                     &deposit.coin.h_age_commitment, -                                   NULL /* h_extensions! */, +                                   NULL /* FIXME: h_extensions! */,                                     &deposit.coin.denom_pub_hash,                                     deposit.timestamp,                                     &deposit.merchant_pub, @@ -481,7 +481,7 @@ TEH_handler_deposit (struct MHD_Connection *connection,      res = reply_deposit_success (connection,                                   &deposit.coin.coin_pub,                                   &h_wire, -                                 NULL /* h_extensions! */, +                                 NULL /* FIXME: h_extensions! */,                                   &deposit.h_contract_terms,                                   dc.exchange_timestamp,                                   deposit.refund_deadline, diff --git a/src/exchange/taler-exchange-httpd_deposits_get.c b/src/exchange/taler-exchange-httpd_deposits_get.c index 97618a94..eec81569 100644 --- a/src/exchange/taler-exchange-httpd_deposits_get.c +++ b/src/exchange/taler-exchange-httpd_deposits_get.c @@ -40,12 +40,12 @@ struct DepositWtidContext    /**     * Hash over the proposal data of the contract for which this deposit is made.     */ -  struct TALER_PrivateContractHashP h_contract_terms GNUNET_PACKED; +  struct TALER_PrivateContractHashP h_contract_terms;    /**     * Hash over the wiring information of the merchant.     */ -  struct TALER_MerchantWireHashP h_wire GNUNET_PACKED; +  struct TALER_MerchantWireHashP h_wire;    /**     * The Merchant's public key.  The deposit inquiry request is to be @@ -251,8 +251,12 @@ handle_track_transaction_request (      return TALER_MHD_REPLY_JSON_PACK (        connection,        MHD_HTTP_ACCEPTED, -      GNUNET_JSON_pack_uint64 ("payment_target_uuid", -                               ctx->kyc.payment_target_uuid), +      GNUNET_JSON_pack_allow_null ( +        (0 == ctx->kyc.payment_target_uuid) +        ? GNUNET_JSON_pack_string ("legitimization_uuid", +                                   NULL) +        : GNUNET_JSON_pack_uint64 ("legitimization_uuid", +                                   ctx->kyc.payment_target_uuid)),        GNUNET_JSON_pack_bool ("kyc_ok",                               ctx->kyc.ok),        GNUNET_JSON_pack_timestamp ("execution_time", diff --git a/src/exchange/taler-exchange-httpd_keys.c b/src/exchange/taler-exchange-httpd_keys.c index f9c35eee..729e627e 100644 --- a/src/exchange/taler-exchange-httpd_keys.c +++ b/src/exchange/taler-exchange-httpd_keys.c @@ -21,6 +21,7 @@  #include "platform.h"  #include "taler_json_lib.h"  #include "taler_mhd_lib.h" +#include "taler_kyclogic_lib.h"  #include "taler_dbevents.h"  #include "taler-exchange-httpd.h"  #include "taler-exchange-httpd_keys.h" @@ -1722,6 +1723,27 @@ setup_general_response_headers (struct TEH_KeyStateHandle *ksh,  /** + * Function called with wallet balance threshholds. + * + * @param[in,out] cls a `json **` where to put the array of json amounts discovered + * @param threshold another threshold amount to add + */ +static void +wallet_threshold_cb (void *cls, +                     const struct TALER_Amount *threshold) +{ +  json_t **ret = cls; + +  if (NULL == *ret) +    *ret = json_array (); +  GNUNET_assert (0 == +                 json_array_append_new (*ret, +                                        TALER_JSON_from_amount ( +                                          threshold))); +} + + +/**   * Initialize @a krd using the given values for @a signkeys,   * @a recoup and @a denoms.   * @@ -1854,17 +1876,20 @@ create_krd (struct TEH_KeyStateHandle *ksh,    GNUNET_assert (NULL != keys);    /* Set wallet limit if KYC is configured */ -  if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && -       (GNUNET_OK == -        TALER_amount_is_valid (&TEH_kyc_config.wallet_balance_limit)) )    { -    GNUNET_assert ( -      0 == -      json_object_set_new ( -        keys, -        "wallet_balance_limit_without_kyc", -        TALER_JSON_from_amount ( -          &TEH_kyc_config.wallet_balance_limit))); +    json_t *wblwk = NULL; + +    TALER_KYCLOGIC_kyc_iterate_thresholds ( +      TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE, +      &wallet_threshold_cb, +      &wblwk); +    if (NULL != wblwk) +      GNUNET_assert ( +        0 == +        json_object_set_new ( +          keys, +          "wallet_balance_limit_without_kyc", +          wblwk));    }    /* Signal support for the configured, enabled extensions. */ diff --git a/src/exchange/taler-exchange-httpd_kyc-check.c b/src/exchange/taler-exchange-httpd_kyc-check.c index b4ec4197..61d1abf8 100644 --- a/src/exchange/taler-exchange-httpd_kyc-check.c +++ b/src/exchange/taler-exchange-httpd_kyc-check.c @@ -1,6 +1,6 @@  /*    This file is part of TALER -  Copyright (C) 2021 Taler Systems SA +  Copyright (C) 2021-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 @@ -25,6 +25,7 @@  #include <microhttpd.h>  #include <pthread.h>  #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h"  #include "taler_mhd_lib.h"  #include "taler_signatures.h"  #include "taler_dbevents.h" @@ -54,6 +55,17 @@ struct KycPoller    struct MHD_Connection *connection;    /** +   * Logic for @e ih +   */ +  struct TALER_KYCLOGIC_Plugin *ih_logic; + +  /** +   * Handle to asynchronously running KYC initiation +   * request. +   */ +  struct TALER_KYCLOGIC_InitiateHandle *ih; + +  /**     * Subscription for the database event we are     * waiting for.     */ @@ -62,12 +74,7 @@ struct KycPoller    /**     * UUID being checked.     */ -  uint64_t auth_payment_target_uuid; - -  /** -   * Current KYC status. -   */ -  struct TALER_EXCHANGEDB_KycStatus kyc; +  uint64_t legitimization_uuid;    /**     * Hash of the payto:// URI we are confirming to @@ -76,20 +83,45 @@ struct KycPoller    struct TALER_PaytoHashP h_payto;    /** -   * Payto URL as a string, as given to us by t +   * When will this request time out?     */ -  const char *hps; +  struct GNUNET_TIME_Absolute timeout;    /** -   * When will this request time out? +   * Type of KYC check required for this client.     */ -  struct GNUNET_TIME_Absolute timeout; +  char *required; + +  /** +   * Set to starting URL of KYC process if KYC is required. +   */ +  char *kyc_url; + +  /** +   * Set to error details, on error (@ec not TALER_EC_NONE). +   */ +  char *hint; + +  /** +   * Set to error encountered with KYC logic, if any. +   */ +  enum TALER_ErrorCode ec;    /**     * True if we are still suspended.     */    bool suspended; +  /** +   * True if KYC was required but is fully satisfied. +   */ +  bool found; + +  /** +   * True if we once tried the KYC initiation. +   */ +  bool ih_done; +  }; @@ -114,6 +146,11 @@ TEH_kyc_check_cleanup ()      GNUNET_CONTAINER_DLL_remove (kyp_head,                                   kyp_tail,                                   kyp); +    if (NULL != kyp->ih) +    { +      kyp->ih_logic->initiate_cancel (kyp->ih); +      kyp->ih = NULL; +    }      if (kyp->suspended)      {        kyp->suspended = false; @@ -143,11 +180,84 @@ kyp_cleanup (struct TEH_RequestContext *rc)                                       kyp->eh);      kyp->eh = NULL;    } +  if (NULL != kyp->ih) +  { +    kyp->ih_logic->initiate_cancel (kyp->ih); +    kyp->ih = NULL; +  } +  GNUNET_free (kyp->kyc_url); +  GNUNET_free (kyp->hint); +  GNUNET_free (kyp->required);    GNUNET_free (kyp);  }  /** + * Function called with the result of a KYC initiation + * operation. + * + * @param cls closure with our `struct KycPoller *` + * @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) +{ +  struct KycPoller *kyp = cls; +  enum GNUNET_DB_QueryStatus qs; + +  kyp->ih = NULL; +  kyp->ih_done = true; +  GNUNET_log (GNUNET_ERROR_TYPE_INFO, +              "KYC initiation completed with status %d (%s)\n", +              ec, +              (TALER_EC_NONE == ec) +              ? redirect_url +              : error_msg_hint); +  kyp->ec = ec; +  if (TALER_EC_NONE == ec) +  { +    kyp->kyc_url = GNUNET_strdup (redirect_url); +  } +  else +  { +    kyp->hint = GNUNET_strdup (error_msg_hint); +  } +  qs = TEH_plugin->update_kyc_requirement_by_row ( +    TEH_plugin->cls, +    kyp->legitimization_uuid, +    kyp->required, +    &kyp->h_payto, +    provider_user_id, +    provider_legitimization_id, +    GNUNET_TIME_UNIT_ZERO_ABS); +  if (qs < 0) +    GNUNET_log (GNUNET_ERROR_TYPE_ERROR, +                "KYC requirement update failed for %s with status %d at %s:%u\n", +                TALER_B2S (&kyp->h_payto), +                qs, +                __FILE__, +                __LINE__); +  GNUNET_assert (kyp->suspended); +  kyp->suspended = false; +  GNUNET_CONTAINER_DLL_remove (kyp_head, +                               kyp_tail, +                               kyp); +  MHD_resume_connection (kyp->connection); +  TALER_MHD_daemon_trigger (); +} + + +/**   * Function implementing database transaction to check wallet's KYC status.   * 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 @@ -168,21 +278,82 @@ kyc_check (void *cls,  {    struct KycPoller *kyp = cls;    enum GNUNET_DB_QueryStatus qs; - -  qs = TEH_plugin->select_kyc_status (TEH_plugin->cls, -                                      &kyp->h_payto, -                                      &kyp->kyc); +  struct TALER_KYCLOGIC_ProviderDetails *pd; +  enum GNUNET_GenericReturnValue ret; +  struct TALER_PaytoHashP h_payto; +  struct GNUNET_TIME_Absolute expiration; +  char *provider_account_id; +  char *provider_legitimization_id; + +  qs = TEH_plugin->lookup_kyc_requirement_by_row ( +    TEH_plugin->cls, +    kyp->legitimization_uuid, +    &kyp->required, +    &h_payto, +    &expiration, +    &provider_account_id, +    &provider_legitimization_id); +  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) +  { +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "No KYC requirements open for %llu\n", +                (unsigned long long) kyp->legitimization_uuid); +    return qs; +  }    if (qs < 0)    { -    if (GNUNET_DB_STATUS_SOFT_ERROR == qs) -      return qs; -    GNUNET_break (0); +    GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); +    return qs; +  } +  GNUNET_free (provider_account_id); +  GNUNET_free (provider_legitimization_id); +  if (0 != +      GNUNET_memcmp (&kyp->h_payto, +                     &h_payto)) +  { +    GNUNET_log (GNUNET_ERROR_TYPE_WARNING, +                "Account %llu provided, but h_payto does not match\n", +                (unsigned long long) kyp->legitimization_uuid); +    GNUNET_break_op (0);      *mhd_ret = TALER_MHD_reply_with_error (connection, -                                           MHD_HTTP_INTERNAL_SERVER_ERROR, -                                           TALER_EC_GENERIC_DB_FETCH_FAILED, -                                           "inselect_wallet_status"); +                                           MHD_HTTP_FORBIDDEN, +                                           TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED, +                                           "h_payto"); +    return GNUNET_DB_STATUS_HARD_ERROR; +  } +  kyp->found = true; +  if (GNUNET_TIME_absolute_is_future (expiration)) +  { +    /* kyc not required, we are done */      return qs;    } + +  ret = TALER_KYCLOGIC_kyc_get_logic (kyp->required, +                                      &kyp->ih_logic, +                                      &pd); +  if (GNUNET_OK != ret) +  { +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "KYC logic for `%s' not configured but used in database!\n", +                kyp->required); +    *mhd_ret = TALER_MHD_reply_with_error (connection, +                                           MHD_HTTP_INTERNAL_SERVER_ERROR, +                                           TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_GONE, +                                           kyp->required); +    return GNUNET_DB_STATUS_HARD_ERROR; +  } +  if (kyp->ih_done) +    return qs; +  GNUNET_log (GNUNET_ERROR_TYPE_INFO, +              "Initiating KYC check with logic %s\n", +              kyp->required); +  kyp->ih = kyp->ih_logic->initiate (kyp->ih_logic->cls, +                                     pd, +                                     &h_payto, +                                     kyp->legitimization_uuid, +                                     &initiate_cb, +                                     kyp); +  GNUNET_break (NULL != kyp->ih);    return qs;  } @@ -230,7 +401,7 @@ db_event_cb (void *cls,  MHD_RESULT  TEH_handler_kyc_check (    struct TEH_RequestContext *rc, -  const char *const args[]) +  const char *const args[2])  {    struct KycPoller *kyp = rc->rh_ctx;    MHD_RESULT res; @@ -245,24 +416,37 @@ TEH_handler_kyc_check (      rc->rh_cleaner = &kyp_cleanup;      { -      // FIXME: now 'legitimization_uuid'! -      unsigned long long payment_target_uuid; +      unsigned long long legitimization_uuid;        char dummy;        if (1 !=            sscanf (args[0],                    "%llu%c", -                  &payment_target_uuid, +                  &legitimization_uuid,                    &dummy))        {          GNUNET_break_op (0);          return TALER_MHD_reply_with_error (rc->connection,                                             MHD_HTTP_BAD_REQUEST,                                             TALER_EC_GENERIC_PARAMETER_MALFORMED, -                                           "payment_target_uuid"); +                                           "legitimization_uuid");        } -      kyp->auth_payment_target_uuid = (uint64_t) payment_target_uuid; +      kyp->legitimization_uuid = (uint64_t) legitimization_uuid; +    } + +    if (GNUNET_OK != +        GNUNET_STRINGS_string_to_data (args[1], +                                       strlen (args[1]), +                                       &kyp->h_payto, +                                       sizeof (kyp->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");      } +      {        const char *ts; @@ -291,41 +475,8 @@ TEH_handler_kyc_check (                                           tms));        }      } - -    // FIXME: replace with args[1]! -    kyp->hps = MHD_lookup_connection_value (rc->connection, -                                            MHD_GET_ARGUMENT_KIND, -                                            "h_payto"); -    if (NULL == kyp->hps) -    { -      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 (kyp->hps, -                                       strlen (kyp->hps), -                                       &kyp->h_payto, -                                       sizeof (kyp->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 (TEH_KYC_NONE == TEH_kyc_config.mode) -    return TALER_MHD_reply_static ( -      rc->connection, -      MHD_HTTP_NO_CONTENT, -      NULL, -      NULL, -      0); -    if ( (NULL == kyp->eh) &&         GNUNET_TIME_absolute_is_future (kyp->timeout) )    { @@ -353,27 +504,48 @@ TEH_handler_kyc_check (                                  &kyc_check,                                  kyp);    if (GNUNET_SYSERR == ret) +  { +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "Transaction failed.\n");      return res; +  } -  if (kyp->auth_payment_target_uuid != -      kyp->kyc.payment_target_uuid) +  if ( (NULL == kyp->ih) && +       (! kyp->found) )    { -    GNUNET_log (GNUNET_ERROR_TYPE_WARNING, -                "Account %llu provided, but payto %s is for %llu\n", -                (unsigned long long) kyp->auth_payment_target_uuid, -                kyp->hps, -                (unsigned long long) kyp->kyc.payment_target_uuid); -    GNUNET_break_op (0); -    return TALER_MHD_reply_with_error (rc->connection, -                                       MHD_HTTP_FORBIDDEN, -                                       TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED, -                                       "h_payto"); +    /* KYC not required */ +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "KYC not required %llu\n", +                (unsigned long long) kyp->legitimization_uuid); +    return TALER_MHD_reply_static ( +      rc->connection, +      MHD_HTTP_NO_CONTENT, +      NULL, +      NULL, +      0); +  } + +  if (NULL != kyp->ih) +  { +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "Suspending HTTP request on KYC logic...\n"); +    kyp->suspended = true; +    GNUNET_CONTAINER_DLL_insert (kyp_head, +                                 kyp_tail, +                                 kyp); +    MHD_suspend_connection (kyp->connection); +    return MHD_YES;    }    /* long polling? */ -  if ( (! kyp->kyc.ok) && +  if ( (NULL != kyp->required) &&         GNUNET_TIME_absolute_is_future (kyp->timeout))    { +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "Suspending HTTP request on timeout (%s) now...\n", +                GNUNET_TIME_relative2s (GNUNET_TIME_absolute_get_duration ( +                                          kyp->timeout), +                                        true));      GNUNET_assert (NULL != kyp->eh);      kyp->suspended = true;      GNUNET_CONTAINER_DLL_insert (kyp_head, @@ -383,37 +555,24 @@ TEH_handler_kyc_check (      return MHD_YES;    } -  /* KYC failed? */ -  if (! kyp->kyc.ok) +  /* KYC plugin generated reply? */ +  if (NULL != kyp->kyc_url)    { -    char *url; -    char *redirect_uri; -    char *redirect_uri_encoded; - -    GNUNET_assert (TEH_KYC_OAUTH2 == TEH_kyc_config.mode); -    GNUNET_asprintf (&redirect_uri, -                     "%s/kyc-proof/%s", -                     TEH_base_url, -                     kyp->hps); -    redirect_uri_encoded = TALER_urlencode (redirect_uri); -    GNUNET_free (redirect_uri); -    GNUNET_asprintf (&url, -                     "%s?client_id=%s&redirect_uri=%s", -                     TEH_kyc_config.details.oauth2.login_url, -                     TEH_kyc_config.details.oauth2.client_id, -                     redirect_uri_encoded); -    GNUNET_free (redirect_uri_encoded); - -    res = TALER_MHD_REPLY_JSON_PACK ( +    return TALER_MHD_REPLY_JSON_PACK (        rc->connection,        MHD_HTTP_ACCEPTED,        GNUNET_JSON_pack_string ("kyc_url", -                               url)); -    GNUNET_free (url); -    return res; +                               kyp->kyc_url)); +  } + +  if (TALER_EC_NONE != kyp->ec) +  { +    return TALER_MHD_reply_with_ec (rc->connection, +                                    kyp->ec, +                                    kyp->hint);    } -  /* KYC succeeded! */ +  /* KYC must have succeeded! */    {      struct TALER_ExchangePublicKeyP pub;      struct TALER_ExchangeSignatureP sig; diff --git a/src/exchange/taler-exchange-httpd_kyc-proof.c b/src/exchange/taler-exchange-httpd_kyc-proof.c index 75ff81e9..64694d28 100644 --- a/src/exchange/taler-exchange-httpd_kyc-proof.c +++ b/src/exchange/taler-exchange-httpd_kyc-proof.c @@ -1,6 +1,6 @@  /*    This file is part of TALER -  Copyright (C) 2021 Taler Systems SA +  Copyright (C) 2021-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 @@ -25,6 +25,7 @@  #include <microhttpd.h>  #include <pthread.h>  #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h"  #include "taler_mhd_lib.h"  #include "taler-exchange-httpd_kyc-proof.h"  #include "taler-exchange-httpd_responses.h" @@ -52,30 +53,36 @@ struct KycProofContext    struct TEH_RequestContext *rc;    /** -   * Handle for the OAuth 2.0 CURL request. +   * Proof logic to run.     */ -  struct GNUNET_CURL_Job *job; +  struct TALER_KYCLOGIC_Plugin *logic;    /** -   * OAuth 2.0 authorization code. +   * Configuration for @a logic.     */ -  const char *authorization_code; +  struct TALER_KYCLOGIC_ProviderDetails *pd; + +  /** +   * Asynchronous operation with the proof system. +   */ +  struct TALER_KYCLOGIC_ProofHandle *ph;    /** -   * OAuth 2.0 token URL we are using for the -   * request. +   * Process information about the user for the plugin from the database, can +   * be NULL.     */ -  char *token_url; +  char *provider_user_id;    /** -   * Body of the POST request. +   * Process information about the legitimization process for the plugin from the +   * database, can be NULL.     */ -  char *post_body; +  char *provider_legitimization_id;    /** -   * User ID extracted from the OAuth 2.0 service, or NULL. +   * OAuth 2.0 authorization code.     */ -  char *id; +  const char *authorization_code;    /**     * Hash of payment target URI this is about. @@ -88,16 +95,24 @@ struct KycProofContext    struct MHD_Response *response;    /** +   * Configuration section for the logic we are running. +   */ +  char *provider_section; + +  /** +   * Row in the database for this legitimization operation. +   */ +  uint64_t legi_row; + +  /**     * 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. +   * True if we are suspended,     */ -  enum GNUNET_GenericReturnValue suspended; +  bool suspended;  }; @@ -122,7 +137,7 @@ static void  kpc_resume (struct KycProofContext *kpc)  {    GNUNET_assert (GNUNET_YES == kpc->suspended); -  kpc->suspended = GNUNET_NO; +  kpc->suspended = false;    GNUNET_CONTAINER_DLL_remove (kpc_head,                                 kpc_tail,                                 kpc); @@ -138,10 +153,10 @@ TEH_kyc_proof_cleanup (void)    while (NULL != (kpc = kpc_head))    { -    if (NULL != kpc->job) +    if (NULL != kpc->ph)      { -      GNUNET_CURL_job_cancel (kpc->job); -      kpc->job = NULL; +      kpc->logic->proof_cancel (kpc->ph); +      kpc->ph = NULL;      }      kpc_resume (kpc);    } @@ -149,348 +164,69 @@ TEH_kyc_proof_cleanup (void)  /** - * Function implementing database transaction to check proof's KYC status. - * 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. + * Function called with the result of a proof check operation.   * - * @param cls closure with a `struct KycProofContext *` - * @param connection MHD proof 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 -persist_kyc_ok (void *cls, -                struct MHD_Connection *connection, -                MHD_RESULT *mhd_ret) -{ -  struct KycProofContext *kpc = cls; -  enum GNUNET_DB_QueryStatus qs; - -  qs = TEH_plugin->set_kyc_ok (TEH_plugin->cls, -                               &kpc->h_payto, -                               kpc->id); -  if (GNUNET_DB_STATUS_HARD_ERROR == qs) -  { -    GNUNET_break (0); -    *mhd_ret = TALER_MHD_reply_with_ec (connection, -                                        TALER_EC_GENERIC_DB_STORE_FAILED, -                                        "set_kyc_ok"); -  } -  return qs; -} - - -/** - * The request for @a kpc failed. We may have gotten a useful error - * message in @a j. Generate a failure response. + * Note that the "decref" for the @a response + * will be done by the callee and MUST NOT be done by the plugin.   * - * @param[in,out] kpc request that failed - * @param j reply from the server (or NULL) + * @param cls closure + * @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 http_status HTTP status code of @a response + * @param[in] response to return to the HTTP client   */  static void -handle_error (struct KycProofContext *kpc, -              const json_t *j) +proof_cb ( +  void *cls, +  enum TALER_KYCLOGIC_KycStatus status, +  const char *provider_user_id, +  const char *provider_legitimization_id, +  struct GNUNET_TIME_Absolute expiration, +  unsigned int http_status, +  struct MHD_Response *response)  { -  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 () -  }; - -  { -    enum GNUNET_GenericReturnValue res; -    const char *emsg; -    unsigned int line; - -    res = GNUNET_JSON_parse (j, -                             spec, -                             &emsg, -                             &line); -    if (GNUNET_OK != res) -    { -      GNUNET_break_op (0); -      kpc->response -        = TALER_MHD_make_error ( -            TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, -            "Unexpected response from KYC gateway"); -      kpc->response_code -        = MHD_HTTP_BAD_GATEWAY; -      return; -    } -  } -  /* case TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_AUTHORZATION_FAILED, -     we MAY want to in the future look at the requested content type -     and possibly respond in JSON if indicated. */ -  { -    char *reply; - -    GNUNET_asprintf (&reply, -                     "<html><head><title>%s</title></head><body><h1>%s</h1><p>%s</p></body></html>", -                     msg, -                     msg, -                     desc); -    kpc->response -      = MHD_create_response_from_buffer (strlen (reply), -                                         reply, -                                         MHD_RESPMEM_MUST_COPY); -    GNUNET_assert (NULL != kpc->response); -    GNUNET_free (reply); -  } -  kpc->response_code = MHD_HTTP_FORBIDDEN; -} +  struct KycProofContext *kpc = cls; +  struct TEH_RequestContext *rc = kpc->rc; +  struct GNUNET_AsyncScopeSave old_scope; +  kpc->ph = NULL; +  GNUNET_async_scope_enter (&rc->async_scope_id, +                            &old_scope); -/** - * The request for @a kpc succeeded (presumably). - * Parse the user ID and store it in @a kpc (if possible). - * - * @param[in,out] kpc request that succeeded - * @param j reply from the server - */ -static void -parse_success_reply (struct KycProofContext *kpc, -                     const json_t *j) -{ -  const char *state; -  json_t *data; -  struct GNUNET_JSON_Specification spec[] = { -    GNUNET_JSON_spec_string ("status", -                             &state), -    GNUNET_JSON_spec_json ("data", -                           &data), -    GNUNET_JSON_spec_end () -  }; -  enum GNUNET_GenericReturnValue res; -  const char *emsg; -  unsigned int line; - -  res = GNUNET_JSON_parse (j, -                           spec, -                           &emsg, -                           &line); -  if (GNUNET_OK != res) +  if (TALER_KYCLOGIC_STATUS_SUCCESS == status)    { -    GNUNET_break_op (0); -    kpc->response -      = TALER_MHD_make_error ( -          TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, -          "Unexpected response from KYC gateway"); -    kpc->response_code -      = MHD_HTTP_BAD_GATEWAY; -    return; -  } -  if (0 != strcasecmp (state, -                       "success")) -  { -    GNUNET_break_op (0); -    handle_error (kpc, -                  j); -    return; -  } -  { -    const char *id; -    struct GNUNET_JSON_Specification ispec[] = { -      GNUNET_JSON_spec_string ("id", -                               &id), -      GNUNET_JSON_spec_end () -    }; - -    res = GNUNET_JSON_parse (data, -                             ispec, -                             &emsg, -                             &line); -    if (GNUNET_OK != res) +    enum GNUNET_DB_QueryStatus qs; + +    qs = TEH_plugin->update_kyc_requirement_by_row (TEH_plugin->cls, +                                                    kpc->legi_row, +                                                    kpc->provider_section, +                                                    &kpc->h_payto, +                                                    provider_user_id, +                                                    provider_legitimization_id, +                                                    expiration); +    if (GNUNET_DB_STATUS_HARD_ERROR == qs)      { -      GNUNET_break_op (0); -      kpc->response -        = TALER_MHD_make_error ( -            TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, -            "Unexpected response from KYC gateway"); -      kpc->response_code -        = MHD_HTTP_BAD_GATEWAY; +      GNUNET_break (0); +      kpc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; +      kpc->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, +                                            "set_kyc_ok"); +      GNUNET_async_scope_restore (&old_scope);        return;      } -    kpc->id = GNUNET_strdup (id); -  } -} - - -/** - * After we are done with the CURL interaction we - * need to update our database state with the information - * retrieved. - * - * @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_fetch_finished (void *cls, -                            long response_code, -                            const void *response) -{ -  struct KycProofContext *kpc = cls; -  const json_t *j = response; - -  kpc->job = NULL; -  switch (response_code) -  { -  case MHD_HTTP_OK: -    parse_success_reply (kpc, -                         j); -    break; -  default: -    handle_error (kpc, -                  j); -    break;    } -  kpc_resume (kpc); -} - - -/** - * 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 KycProofContext *kpc = cls; -  const json_t *j = response; - -  kpc->job = NULL; -  switch (response_code) +  else    { -  case MHD_HTTP_OK: -    { -      const char *access_token; -      const char *token_type; -      uint64_t expires_in_s; -      const char *refresh_token; -      struct GNUNET_JSON_Specification spec[] = { -        GNUNET_JSON_spec_string ("access_token", -                                 &access_token), -        GNUNET_JSON_spec_string ("token_type", -                                 &token_type), -        GNUNET_JSON_spec_uint64 ("expires_in", -                                 &expires_in_s), -        GNUNET_JSON_spec_string ("refresh_token", -                                 &refresh_token), -        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) -        { -          GNUNET_break_op (0); -          kpc->response -            = TALER_MHD_make_error ( -                TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, -                "Unexpected response from KYC gateway"); -          kpc->response_code -            = MHD_HTTP_BAD_GATEWAY; -          break; -        } -      } -      if (0 != strcasecmp (token_type, -                           "bearer")) -      { -        GNUNET_break_op (0); -        kpc->response -          = TALER_MHD_make_error ( -              TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, -              "Unexpected token type in response from KYC gateway"); -        kpc->response_code -          = MHD_HTTP_BAD_GATEWAY; -        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, -                            ';')) ) -      { -        GNUNET_break_op (0); -        kpc->response -          = TALER_MHD_make_error ( -              TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, -              "Illegal character in access token"); -        kpc->response_code -          = MHD_HTTP_BAD_GATEWAY; -        break; -      } - -      eh = curl_easy_init (); -      if (NULL == eh) -      { -        GNUNET_break_op (0); -        kpc->response -          = TALER_MHD_make_error ( -              TALER_EC_GENERIC_ALLOCATION_FAILURE, -              "curl_easy_init"); -        kpc->response_code -          = MHD_HTTP_INTERNAL_SERVER_ERROR; -        break; -      } -      GNUNET_assert (CURLE_OK == -                     curl_easy_setopt (eh, -                                       CURLOPT_URL, -                                       TEH_kyc_config.details.oauth2.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); -        kpc->job = GNUNET_CURL_job_add2 (TEH_curl_ctx, -                                         eh, -                                         slist, -                                         &handle_curl_fetch_finished, -                                         kpc); -        curl_slist_free_all (slist); -        GNUNET_free (hdr); -      } -      return; -    } -  default: -    handle_error (kpc, -                  j); -    break; +    GNUNET_log (GNUNET_ERROR_TYPE_INFO, +                "KYC logic #%llu failed with status %d\n", +                (unsigned long long) kpc->legi_row, +                status);    } +  kpc->response_code = http_status; +  kpc->response = response;    kpc_resume (kpc); +  GNUNET_async_scope_restore (&old_scope);  } @@ -504,19 +240,19 @@ clean_kpc (struct TEH_RequestContext *rc)  {    struct KycProofContext *kpc = rc->rh_ctx; -  if (NULL != kpc->job) +  if (NULL != kpc->ph)    { -    GNUNET_CURL_job_cancel (kpc->job); -    kpc->job = NULL; +    kpc->logic->proof_cancel (kpc->ph); +    kpc->ph = NULL;    }    if (NULL != kpc->response)    {      MHD_destroy_response (kpc->response);      kpc->response = NULL;    } -  GNUNET_free (kpc->post_body); -  GNUNET_free (kpc->token_url); -  GNUNET_free (kpc->id); +  GNUNET_free (kpc->provider_user_id); +  GNUNET_free (kpc->provider_legitimization_id); +  GNUNET_free (kpc->provider_section);    GNUNET_free (kpc);  } @@ -529,7 +265,18 @@ TEH_handler_kyc_proof (    struct KycProofContext *kpc = rc->rh_ctx;    if (NULL == kpc) -  { /* first time */ +  { +    /* first time */ +    if ( (NULL == args[0]) || +         (NULL == args[1]) ) +    { +      GNUNET_break_op (0); +      return TALER_MHD_reply_with_error (rc->connection, +                                         MHD_HTTP_NOT_FOUND, +                                         TALER_EC_GENERIC_ENDPOINT_UNKNOWN, +                                         "'/kyc-proof/$H_PATYO/$LOGIC' required"); +    } +      kpc = GNUNET_new (struct KycProofContext);      kpc->rc = rc;      rc->rh_ctx = kpc; @@ -546,166 +293,98 @@ TEH_handler_kyc_proof (                                           TALER_EC_GENERIC_PARAMETER_MALFORMED,                                           "h_payto");      } -    kpc->authorization_code -      = MHD_lookup_connection_value (rc->connection, -                                     MHD_GET_ARGUMENT_KIND, -                                     "code"); -    if (NULL == kpc->authorization_code) +    kpc->provider_section = GNUNET_strdup (args[1]); +    if (GNUNET_OK != +        TALER_KYCLOGIC_kyc_get_logic (kpc->provider_section, +                                      &kpc->logic, +                                      &kpc->pd))      {        GNUNET_break_op (0);        return TALER_MHD_reply_with_error (rc->connection, -                                         MHD_HTTP_BAD_REQUEST, -                                         TALER_EC_GENERIC_PARAMETER_MALFORMED, -                                         "code"); +                                         MHD_HTTP_NOT_FOUND, +                                         TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, +                                         kpc->provider_section);      } -    if (TEH_KYC_NONE == TEH_kyc_config.mode) -      return TALER_MHD_reply_static ( -        rc->connection, -        MHD_HTTP_NO_CONTENT, -        NULL, -        NULL, -        0);      { -      CURL *eh; - -      eh = curl_easy_init (); -      if (NULL == eh) +      enum GNUNET_DB_QueryStatus qs; +      struct GNUNET_TIME_Absolute expiration; + +      qs = TEH_plugin->lookup_kyc_requirement_by_account ( +        TEH_plugin->cls, +        kpc->provider_section, +        &kpc->h_payto, +        &kpc->legi_row, +        &expiration, +        &kpc->provider_user_id, +        &kpc->provider_legitimization_id); +      switch (qs)        { -        GNUNET_break (0); +      case GNUNET_DB_STATUS_HARD_ERROR: +      case GNUNET_DB_STATUS_SOFT_ERROR: +        return TALER_MHD_reply_with_ec (rc->connection, +                                        TALER_EC_GENERIC_DB_STORE_FAILED, +                                        "lookup_kyc_requirement_by_account"); +      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:          return TALER_MHD_reply_with_error (rc->connection, -                                           MHD_HTTP_INTERNAL_SERVER_ERROR, -                                           TALER_EC_GENERIC_ALLOCATION_FAILURE, -                                           "curl_easy_init"); +                                           MHD_HTTP_NOT_FOUND, +                                           TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, +                                           kpc->provider_section); +      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: +        break;        } -      GNUNET_asprintf (&kpc->token_url, -                       "%s", -                       TEH_kyc_config.details.oauth2.auth_url); -      GNUNET_assert (CURLE_OK == -                     curl_easy_setopt (eh, -                                       CURLOPT_URL, -                                       kpc->token_url)); -      GNUNET_assert (CURLE_OK == -                     curl_easy_setopt (eh, -                                       CURLOPT_POST, -                                       1)); +      if (GNUNET_TIME_absolute_is_future (expiration))        { -        char *client_id; -        char *redirect_uri; -        char *client_secret; -        char *authorization_code; - -        client_id = curl_easy_escape (eh, -                                      TEH_kyc_config.details.oauth2.client_id, -                                      0); -        GNUNET_assert (NULL != client_id); -        { -          char *request_uri; - -          GNUNET_asprintf (&request_uri, -                           "%s?client_id=%s", -                           TEH_kyc_config.details.oauth2.login_url, -                           TEH_kyc_config.details.oauth2.client_id); -          redirect_uri = curl_easy_escape (eh, -                                           request_uri, -                                           0); -          GNUNET_free (request_uri); -        } -        GNUNET_assert (NULL != redirect_uri); -        client_secret = curl_easy_escape (eh, -                                          TEH_kyc_config.details.oauth2. -                                          client_secret, -                                          0); -        GNUNET_assert (NULL != client_secret); -        authorization_code = curl_easy_escape (eh, -                                               kpc->authorization_code, -                                               0); -        GNUNET_assert (NULL != authorization_code); -        GNUNET_asprintf (&kpc->post_body, -                         "client_id=%s&redirect_uri=%s&client_secret=%s&code=%s&grant_type=authorization_code", -                         client_id, -                         redirect_uri, -                         client_secret, -                         authorization_code); -        curl_free (authorization_code); -        curl_free (client_secret); -        curl_free (redirect_uri); -        curl_free (client_id); +        /* KYC not required */ +        return TALER_MHD_reply_static ( +          rc->connection, +          MHD_HTTP_NO_CONTENT, +          NULL, +          NULL, +          0);        } -      GNUNET_assert (CURLE_OK == -                     curl_easy_setopt (eh, -                                       CURLOPT_POSTFIELDS, -                                       kpc->post_body)); -      GNUNET_assert (CURLE_OK == -                     curl_easy_setopt (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 (eh, -                                       CURLOPT_MAXREDIRS, -                                       5L)); - -      kpc->job = GNUNET_CURL_job_add (TEH_curl_ctx, -                                      eh, -                                      &handle_curl_login_finished, -                                      kpc); -      kpc->suspended = GNUNET_YES; -      GNUNET_CONTAINER_DLL_insert (kpc_head, -                                   kpc_tail, -                                   kpc); -      MHD_suspend_connection (rc->connection); -      return MHD_YES;      } -  } +    kpc->ph = kpc->logic->proof (kpc->logic->cls, +                                 kpc->pd, +                                 &args[2], +                                 rc->connection, +                                 &kpc->h_payto, +                                 kpc->legi_row, +                                 kpc->provider_user_id, +                                 kpc->provider_legitimization_id, +                                 &proof_cb, +                                 kpc); +    if (NULL == kpc->ph) +    { +      GNUNET_break (0); +      return TALER_MHD_reply_with_error (rc->connection, +                                         MHD_HTTP_INTERNAL_SERVER_ERROR, +                                         TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, +                                         "could not start proof with KYC logic"); +    } -  if (NULL != kpc->response) -  { -    /* handle _failed_ resumed cases */ -    return MHD_queue_response (rc->connection, -                               kpc->response_code, -                               kpc->response); -  } -  /* _successfully_ resumed case */ -  { -    MHD_RESULT res; -    enum GNUNET_GenericReturnValue ret; - -    ret = TEH_DB_run_transaction (kpc->rc->connection, -                                  "check proof kyc", -                                  TEH_MT_REQUEST_OTHER, -                                  &res, -                                  &persist_kyc_ok, -                                  kpc); -    if (GNUNET_SYSERR == ret) -      return res; +    kpc->suspended = true; +    GNUNET_CONTAINER_DLL_insert (kpc_head, +                                 kpc_tail, +                                 kpc); +    MHD_suspend_connection (rc->connection); +    return MHD_YES;    } +  if (NULL == kpc->response)    { -    struct MHD_Response *response; -    MHD_RESULT res; - -    response = MHD_create_response_from_buffer (0, -                                                "", -                                                MHD_RESPMEM_PERSISTENT); -    if (NULL == response) -    { -      GNUNET_break (0); -      return MHD_NO; -    } -    GNUNET_break (MHD_YES == -                  MHD_add_response_header ( -                    response, -                    MHD_HTTP_HEADER_LOCATION, -                    TEH_kyc_config.details.oauth2.post_kyc_redirect_url)); -    res = MHD_queue_response (rc->connection, -                              MHD_HTTP_SEE_OTHER, -                              response); -    MHD_destroy_response (response); -    return res; +    GNUNET_break (0); +    return TALER_MHD_reply_with_error (rc->connection, +                                       MHD_HTTP_INTERNAL_SERVER_ERROR, +                                       TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, +                                       "handler resumed without response");    } + +  /* return response from KYC logic */ +  return MHD_queue_response (rc->connection, +                             kpc->response_code, +                             kpc->response);  } diff --git a/src/exchange/taler-exchange-httpd_kyc-wallet.c b/src/exchange/taler-exchange-httpd_kyc-wallet.c index 0d92efd3..a043de6f 100644 --- a/src/exchange/taler-exchange-httpd_kyc-wallet.c +++ b/src/exchange/taler-exchange-httpd_kyc-wallet.c @@ -1,6 +1,6 @@  /*    This file is part of TALER -  Copyright (C) 2021 Taler Systems SA +  Copyright (C) 2021, 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 @@ -26,6 +26,7 @@  #include <pthread.h>  #include "taler_json_lib.h"  #include "taler_mhd_lib.h" +#include "taler_kyclogic_lib.h"  #include "taler-exchange-httpd_kyc-wallet.h"  #include "taler-exchange-httpd_responses.h" @@ -38,16 +39,55 @@ struct KycRequestContext    /**     * Public key of the reserve/wallet this is about.     */ -  struct TALER_ReservePublicKeyP reserve_pub; +  struct TALER_PaytoHashP h_payto;    /** -   * Current KYC status. +   * Row with the legitimization requirement.     */ -  struct TALER_EXCHANGEDB_KycStatus kyc; +  uint64_t legi_row; + +  /** +   * Balance threshold crossed by the wallet. +   */ +  struct TALER_Amount balance; + +  /** +   * Name of the required check. +   */ +  const char *required; +  };  /** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Returns the wallet balance. + * + * @param cls closure, a `struct KycRequestContext` + * @param limit maximum time-range for which events + *        should be fetched (timestamp in the past) + * @param cb function to call on each event found, + *        events must be returned in reverse chronological + *        order + * @param cb_cls closure for @a cb + */ +static void +balance_iterator (void *cls, +                  struct GNUNET_TIME_Absolute limit, +                  TALER_EXCHANGEDB_KycAmountCallback cb, +                  void *cb_cls) +{ +  struct KycRequestContext *krc = cls; + +  (void) limit; +  cb (cb_cls, +      &krc->balance, +      GNUNET_TIME_absolute_get ()); +} + + +/**   * Function implementing database transaction to check wallet's KYC status.   * 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 @@ -69,9 +109,23 @@ wallet_kyc_check (void *cls,    struct KycRequestContext *krc = cls;    enum GNUNET_DB_QueryStatus qs; -  qs = TEH_plugin->inselect_wallet_kyc_status (TEH_plugin->cls, -                                               &krc->reserve_pub, -                                               &krc->kyc); +  krc->required = TALER_KYCLOGIC_kyc_test_required ( +    TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE, +    &krc->h_payto, +    TEH_plugin->select_satisfied_kyc_processes, +    TEH_plugin->cls, +    &balance_iterator, +    krc); +  GNUNET_log (GNUNET_ERROR_TYPE_WARNING, +              "KYC check required at %s is `%s'\n", +              TALER_amount2s (&krc->balance), +              krc->required); +  if (NULL == krc->required) +    return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; +  qs = TEH_plugin->insert_kyc_requirement_for_account (TEH_plugin->cls, +                                                       krc->required, +                                                       &krc->h_payto, +                                                       &krc->legi_row);    if (qs < 0)    {      if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -80,9 +134,14 @@ wallet_kyc_check (void *cls,      *mhd_ret = TALER_MHD_reply_with_error (connection,                                             MHD_HTTP_INTERNAL_SERVER_ERROR,                                             TALER_EC_GENERIC_DB_FETCH_FAILED, -                                           "inselect_wallet_status"); +                                           "insert_kyc_requirement_for_account");      return qs;    } +  GNUNET_log (GNUNET_ERROR_TYPE_INFO, +              "KYC requirement inserted for wallet %s (%llu, %d)\n", +              TALER_B2S (&krc->h_payto), +              (unsigned long long) krc->legi_row, +              qs);    return qs;  } @@ -95,11 +154,17 @@ TEH_handler_kyc_wallet (  {    struct TALER_ReserveSignatureP reserve_sig;    struct KycRequestContext krc; +  struct TALER_ReservePublicKeyP reserve_pub;    struct GNUNET_JSON_Specification spec[] = {      GNUNET_JSON_spec_fixed_auto ("reserve_sig",                                   &reserve_sig),      GNUNET_JSON_spec_fixed_auto ("reserve_pub", -                                 &krc.reserve_pub), +                                 &reserve_pub), +    // FIXME: add balance threshold crossed to the request +    // to spec and client API! +    TALER_JSON_spec_amount ("balance", +                            TEH_currency, +                            &krc.balance),      GNUNET_JSON_spec_end ()    };    MHD_RESULT res; @@ -115,8 +180,10 @@ TEH_handler_kyc_wallet (      return MHD_YES;   /* failure */    TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; +  // FIXME: add balance threshold crossed to +  // what the wallet signs over!    if (GNUNET_OK != -      TALER_wallet_account_setup_verify (&krc.reserve_pub, +      TALER_wallet_account_setup_verify (&reserve_pub,                                           &reserve_sig))    {      GNUNET_break_op (0); @@ -126,13 +193,19 @@ TEH_handler_kyc_wallet (        TALER_EC_EXCHANGE_KYC_WALLET_SIGNATURE_INVALID,        NULL);    } -  if (TEH_KYC_NONE == TEH_kyc_config.mode) -    return TALER_MHD_reply_static ( -      rc->connection, -      MHD_HTTP_NO_CONTENT, -      NULL, -      NULL, -      0); +  { +    char *payto_uri; + +    payto_uri = TALER_reserve_make_payto (TEH_base_url, +                                          &reserve_pub); +    TALER_payto_hash (payto_uri, +                      &krc.h_payto); +    GNUNET_log (GNUNET_ERROR_TYPE_ERROR, +                "h_payto of wallet %s is %s\n", +                payto_uri, +                TALER_B2S (&krc.h_payto)); +    GNUNET_free (payto_uri); +  }    ret = TEH_DB_run_transaction (rc->connection,                                  "check wallet kyc",                                  TEH_MT_REQUEST_OTHER, @@ -141,11 +214,21 @@ TEH_handler_kyc_wallet (                                  &krc);    if (GNUNET_SYSERR == ret)      return res; +  if (NULL == krc.required) +  { +    /* KYC not required or already satisfied */ +    return TALER_MHD_reply_static ( +      rc->connection, +      MHD_HTTP_NO_CONTENT, +      NULL, +      NULL, +      0); +  }    return TALER_MHD_REPLY_JSON_PACK (      rc->connection,      MHD_HTTP_OK,      GNUNET_JSON_pack_uint64 ("payment_target_uuid", -                             krc.kyc.payment_target_uuid)); +                             krc.legi_row));  } diff --git a/src/exchange/taler-exchange-httpd_purses_merge.c b/src/exchange/taler-exchange-httpd_purses_merge.c index 07bcff17..25d91e1b 100644 --- a/src/exchange/taler-exchange-httpd_purses_merge.c +++ b/src/exchange/taler-exchange-httpd_purses_merge.c @@ -421,11 +421,11 @@ TEH_handler_purses_merge (                "Received payto: `%s'\n",                pcc.payto_uri);    if ( (0 != strncmp (pcc.payto_uri, -                      "payto://taler/", -                      strlen ("payto://taler/"))) && +                      "payto://taler-reserve/", +                      strlen ("payto://taler-reserve/"))) &&         (0 != strncmp (pcc.payto_uri, -                      "payto://taler+http/", -                      strlen ("payto://taler+http/"))) ) +                      "payto://taler-reserve+http/", +                      strlen ("payto://taler-reserve+http/"))) )    {      GNUNET_break_op (0);      return TALER_MHD_reply_with_error ( @@ -436,13 +436,13 @@ TEH_handler_purses_merge (    }    http = (0 == strncmp (pcc.payto_uri, -                        "payto://taler+http/", -                        strlen ("payto://taler+http/"))); +                        "payto://taler-reserve+http/", +                        strlen ("payto://taler-reserve+http/")));    {      const char *host = &pcc.payto_uri[http -                                      ? strlen ("payto://taler+http/") -                                      : strlen ("payto://taler/")]; +                                      ? strlen ("payto://taler-reserve+http/") +                                      : strlen ("payto://taler-reserve/")];      const char *slash = strchr (host,                                  '/'); diff --git a/src/exchange/taler-exchange-httpd_withdraw.c b/src/exchange/taler-exchange-httpd_withdraw.c index ee2f410d..255f7d94 100644 --- a/src/exchange/taler-exchange-httpd_withdraw.c +++ b/src/exchange/taler-exchange-httpd_withdraw.c @@ -1,6 +1,6 @@  /*    This file is part of TALER -  Copyright (C) 2014-2021 Taler Systems SA +  Copyright (C) 2014-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 @@ -27,6 +27,7 @@  #include <gnunet/gnunet_util_lib.h>  #include <jansson.h>  #include "taler_json_lib.h" +#include "taler_kyclogic_lib.h"  #include "taler_mhd_lib.h"  #include "taler-exchange-httpd_withdraw.h"  #include "taler-exchange-httpd_responses.h" @@ -45,15 +46,6 @@ struct WithdrawContext    struct TALER_BlindedCoinHashP h_coin_envelope;    /** -   * Value of the coin being exchanged (matching the denomination key) -   * plus the transaction fee.  We include this in what is being -   * signed so that we can verify a reserve's remaining total balance -   * without needing to access the respective denomination key -   * information each time. -   */ -  struct TALER_Amount amount_with_fee; - -  /**     * Blinded planchet.     */    struct TALER_BlindedPlanchet blinded_planchet; @@ -68,10 +60,67 @@ struct WithdrawContext     */    struct TALER_EXCHANGEDB_KycStatus kyc; +  /** +   * Hash of the payto-URI representing the reserve +   * from which we are withdrawing. +   */ +  struct TALER_PaytoHashP h_payto; + +  /** +   * Current time for the DB transaction. +   */ +  struct GNUNET_TIME_Timestamp now; +  };  /** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Called within a database transaction, so must + * not start a new one. + * + * @param cls closure, identifies the event type and + *        account to iterate over events for + * @param limit maximum time-range for which events + *        should be fetched (timestamp in the past) + * @param cb function to call on each event found, + *        events must be returned in reverse chronological + *        order + * @param cb_cls closure for @a cb + */ +static void +withdraw_amount_cb (void *cls, +                    struct GNUNET_TIME_Absolute limit, +                    TALER_EXCHANGEDB_KycAmountCallback cb, +                    void *cb_cls) +{ +  struct WithdrawContext *wc = cls; +  enum GNUNET_DB_QueryStatus qs; + +  GNUNET_log (GNUNET_ERROR_TYPE_INFO, +              "Signaling amount %s for KYC check\n", +              TALER_amount2s (&wc->collectable.amount_with_fee)); +  if (GNUNET_OK != +      cb (cb_cls, +          &wc->collectable.amount_with_fee, +          wc->now.abs_time)) +    return; +  qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( +    TEH_plugin->cls, +    &wc->h_payto, +    limit, +    cb, +    cb_cls); +  GNUNET_log (GNUNET_ERROR_TYPE_INFO, +              "Got %d additional transactions for this withdrawal and limit %llu\n", +              qs, +              (unsigned long long) limit.abs_value_us); +  GNUNET_break (qs >= 0); +} + + +/**   * Function implementing withdraw 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, @@ -98,25 +147,55 @@ withdraw_transaction (void *cls,    bool found = false;    bool balance_ok = false;    bool nonce_ok = false; -  struct GNUNET_TIME_Timestamp now;    uint64_t ruuid;    const struct TALER_CsNonce *nonce;    const struct TALER_BlindedPlanchet *bp; +  const char *kyc_required; -  now = GNUNET_TIME_timestamp_get (); +  wc->now = GNUNET_TIME_timestamp_get (); +  qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls, +                                        &wc->collectable.reserve_pub, +                                        &wc->h_payto); +  if (qs < 0) +    return qs; +  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) +  { +    *mhd_ret = TALER_MHD_reply_with_error (connection, +                                           MHD_HTTP_NOT_FOUND, +                                           TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, +                                           NULL); +    return GNUNET_DB_STATUS_HARD_ERROR; +  } + +  kyc_required = TALER_KYCLOGIC_kyc_test_required ( +    TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, +    &wc->h_payto, +    TEH_plugin->select_satisfied_kyc_processes, +    TEH_plugin->cls, +    &withdraw_amount_cb, +    wc); +  if (NULL != kyc_required) +  { +    /* insert KYC requirement into DB! */ +    wc->kyc.ok = false; +    return TEH_plugin->insert_kyc_requirement_for_account ( +      TEH_plugin->cls, +      kyc_required, +      &wc->h_payto, +      &wc->kyc.payment_target_uuid); +  } +  wc->kyc.ok = true;    bp = &wc->blinded_planchet; -  nonce = -    (TALER_DENOMINATION_CS == bp->cipher) +  nonce = (TALER_DENOMINATION_CS == bp->cipher)      ? &bp->details.cs_blinded_planchet.nonce      : NULL;    qs = TEH_plugin->do_withdraw (TEH_plugin->cls,                                  nonce,                                  &wc->collectable, -                                now, +                                wc->now,                                  &found,                                  &balance_ok,                                  &nonce_ok, -                                &wc->kyc,                                  &ruuid);    if (0 > qs)    { @@ -153,56 +232,8 @@ withdraw_transaction (void *cls,                                             NULL);      return GNUNET_DB_STATUS_HARD_ERROR;    } -  if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && -       (! wc->kyc.ok) && -       (TALER_EXCHANGEDB_KYC_W2W == wc->kyc.type) ) -  { -    /* Wallet-to-wallet payments _always_ require KYC */ -    *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( -      connection, -      MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, -      GNUNET_JSON_pack_uint64 ("payment_target_uuid", -                               wc->kyc.payment_target_uuid)); -    return GNUNET_DB_STATUS_HARD_ERROR; -  } -  if ( (TEH_KYC_NONE != TEH_kyc_config.mode) && -       (! wc->kyc.ok) && -       (TALER_EXCHANGEDB_KYC_WITHDRAW == wc->kyc.type) && -       (! GNUNET_TIME_relative_is_zero (TEH_kyc_config.withdraw_period)) ) -  { -    /* Withdraws require KYC if above threshold */ -    enum GNUNET_DB_QueryStatus qs2; -    bool below_limit; - -    qs2 = TEH_plugin->do_withdraw_limit_check ( -      TEH_plugin->cls, -      ruuid, -      GNUNET_TIME_absolute_subtract (now.abs_time, -                                     TEH_kyc_config.withdraw_period), -      &TEH_kyc_config.withdraw_limit, -      &below_limit); -    if (0 > qs2) -    { -      GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs2); -      if (GNUNET_DB_STATUS_HARD_ERROR == qs2) -        *mhd_ret = TALER_MHD_reply_with_error (connection, -                                               MHD_HTTP_INTERNAL_SERVER_ERROR, -                                               TALER_EC_GENERIC_DB_FETCH_FAILED, -                                               "do_withdraw_limit_check"); -      return qs2; -    } -    if (! below_limit) -    { -      *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( -        connection, -        MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, -        GNUNET_JSON_pack_uint64 ("payment_target_uuid", -                                 wc->kyc.payment_target_uuid)); -      return GNUNET_DB_STATUS_HARD_ERROR; -    } -  }    if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) -    TEH_METRICS_num_success[TEH_MT_SUCCESS_BATCH_WITHDRAW]++; +    TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW]++;    return qs;  } @@ -274,7 +305,6 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc,            0,            sizeof (wc));    wc.collectable.reserve_pub = *reserve_pub; -    {      enum GNUNET_GenericReturnValue res; @@ -459,6 +489,14 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc,    /* Clean up and send back final response */    GNUNET_JSON_parse_free (spec); +  if (! wc.kyc.ok) +  { +    return TALER_MHD_REPLY_JSON_PACK ( +      rc->connection, +      MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, +      GNUNET_JSON_pack_uint64 ("payment_target_uuid", +                               wc.kyc.payment_target_uuid)); +  }    {      MHD_RESULT ret; | 
