exchange

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

plugin_kyclogic_persona.c (71220B)


      1 /*
      2   This file is part of GNU Taler
      3   Copyright (C) 2022, 2023 Taler Systems SA
      4 
      5   Taler is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   Taler; see the file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file plugin_kyclogic_persona.c
     18  * @brief persona for an authentication flow logic
     19  * @author Christian Grothoff
     20  */
     21 #include "taler/taler_kyclogic_plugin.h"
     22 #include "taler/taler_mhd_lib.h"
     23 #include "taler/taler_curl_lib.h"
     24 #include "taler/taler_json_lib.h"
     25 #include "taler/taler_kyclogic_lib.h"
     26 #include "taler/taler_templating_lib.h"
     27 #include <regex.h>
     28 #include "taler/taler_util.h"
     29 
     30 #define DEBUG 0
     31 
     32 
     33 /**
     34  * Which version of the persona API are we implementing?
     35  */
     36 #define PERSONA_VERSION "2021-07-05"
     37 
     38 /**
     39  * Saves the state of a plugin.
     40  */
     41 struct PluginState
     42 {
     43 
     44   /**
     45    * Our base URL.
     46    */
     47   char *exchange_base_url;
     48 
     49   /**
     50    * Our global configuration.
     51    */
     52   const struct GNUNET_CONFIGURATION_Handle *cfg;
     53 
     54   /**
     55    * Context for CURL operations (useful to the event loop)
     56    */
     57   struct GNUNET_CURL_Context *curl_ctx;
     58 
     59   /**
     60    * Context for integrating @e curl_ctx with the
     61    * GNUnet event loop.
     62    */
     63   struct GNUNET_CURL_RescheduleContext *curl_rc;
     64 
     65   /**
     66    * Authorization token to use when receiving webhooks from the Persona
     67    * service.  Optional.  Note that webhooks are *global* and not per
     68    * template.
     69    */
     70   char *webhook_token;
     71 
     72 
     73 };
     74 
     75 
     76 /**
     77  * Keeps the plugin-specific state for
     78  * a given configuration section.
     79  */
     80 struct TALER_KYCLOGIC_ProviderDetails
     81 {
     82 
     83   /**
     84    * Overall plugin state.
     85    */
     86   struct PluginState *ps;
     87 
     88   /**
     89    * Configuration section that configured us.
     90    */
     91   char *section;
     92 
     93   /**
     94    * Name of the provider, that is @e section without
     95    * the "kyc-provider-" prefix. This is the name used
     96    * in the database.
     97    */
     98   char *provider_name;
     99 
    100   /**
    101    * Salt to use for idempotency.
    102    */
    103   char *salt;
    104 
    105   /**
    106    * Authorization token to use when talking
    107    * to the service.
    108    */
    109   char *auth_token;
    110 
    111   /**
    112    * Template ID for the KYC check to perform.
    113    */
    114   char *template_id;
    115 
    116   /**
    117    * Subdomain to use.
    118    */
    119   char *subdomain;
    120 
    121   /**
    122    * Name of the program we use to convert outputs
    123    * from Persona into our JSON inputs.
    124    */
    125   char *conversion_binary;
    126 
    127   /**
    128    * Where to redirect the client upon completion.
    129    */
    130   char *post_kyc_redirect_url;
    131 
    132   /**
    133    * Validity time for a successful KYC process.
    134    */
    135   struct GNUNET_TIME_Relative validity;
    136 
    137   /**
    138    * Curl-ready authentication header to use.
    139    */
    140   struct curl_slist *slist;
    141 
    142 };
    143 
    144 
    145 /**
    146  * Handle for an initiation operation.
    147  */
    148 struct TALER_KYCLOGIC_InitiateHandle
    149 {
    150 
    151   /**
    152    * Hash of the payto:// URI we are initiating the KYC for.
    153    */
    154   struct TALER_NormalizedPaytoHashP h_payto;
    155 
    156   /**
    157    * UUID being checked.
    158    */
    159   uint64_t legitimization_uuid;
    160 
    161   /**
    162    * Our configuration details.
    163    */
    164   const struct TALER_KYCLOGIC_ProviderDetails *pd;
    165 
    166   /**
    167    * Continuation to call.
    168    */
    169   TALER_KYCLOGIC_InitiateCallback cb;
    170 
    171   /**
    172    * Closure for @a cb.
    173    */
    174   void *cb_cls;
    175 
    176   /**
    177    * Context for #TEH_curl_easy_post(). Keeps the data that must
    178    * persist for Curl to make the upload.
    179    */
    180   struct TALER_CURL_PostContext ctx;
    181 
    182   /**
    183    * Handle for the request.
    184    */
    185   struct GNUNET_CURL_Job *job;
    186 
    187   /**
    188    * URL of the cURL request.
    189    */
    190   char *url;
    191 
    192   /**
    193    * Request-specific headers to use.
    194    */
    195   struct curl_slist *slist;
    196 
    197 };
    198 
    199 
    200 /**
    201  * Handle for an KYC proof operation.
    202  */
    203 struct TALER_KYCLOGIC_ProofHandle
    204 {
    205 
    206   /**
    207    * Overall plugin state.
    208    */
    209   struct PluginState *ps;
    210 
    211   /**
    212    * Our configuration details.
    213    */
    214   const struct TALER_KYCLOGIC_ProviderDetails *pd;
    215 
    216   /**
    217    * Continuation to call.
    218    */
    219   TALER_KYCLOGIC_ProofCallback cb;
    220 
    221   /**
    222    * Closure for @e cb.
    223    */
    224   void *cb_cls;
    225 
    226   /**
    227    * Connection we are handling.
    228    */
    229   struct MHD_Connection *connection;
    230 
    231   /**
    232    * Task for asynchronous execution.
    233    */
    234   struct GNUNET_SCHEDULER_Task *task;
    235 
    236   /**
    237    * Handle for the request.
    238    */
    239   struct GNUNET_CURL_Job *job;
    240 
    241   /**
    242    * URL of the cURL request.
    243    */
    244   char *url;
    245 
    246   /**
    247    * Handle to an external process that converts the
    248    * Persona response to our internal format.
    249    */
    250   struct TALER_JSON_ExternalConversion *ec;
    251 
    252   /**
    253    * Hash of the payto:// URI we are checking the KYC for.
    254    */
    255   struct TALER_NormalizedPaytoHashP h_payto;
    256 
    257   /**
    258    * Row in the legitimization processes of the
    259    * legitimization proof that is being checked.
    260    */
    261   uint64_t process_row;
    262 
    263   /**
    264    * Account ID at the provider.
    265    */
    266   char *provider_user_id;
    267 
    268   /**
    269    * Account ID from the service.
    270    */
    271   char *account_id;
    272 
    273   /**
    274    * Inquiry ID at the provider.
    275    */
    276   char *inquiry_id;
    277 };
    278 
    279 
    280 /**
    281  * Handle for an KYC Web hook operation.
    282  */
    283 struct TALER_KYCLOGIC_WebhookHandle
    284 {
    285 
    286   /**
    287    * Continuation to call when done.
    288    */
    289   TALER_KYCLOGIC_WebhookCallback cb;
    290 
    291   /**
    292    * Closure for @a cb.
    293    */
    294   void *cb_cls;
    295 
    296   /**
    297    * Task for asynchronous execution.
    298    */
    299   struct GNUNET_SCHEDULER_Task *task;
    300 
    301   /**
    302    * Overall plugin state.
    303    */
    304   struct PluginState *ps;
    305 
    306   /**
    307    * Our configuration details.
    308    */
    309   const struct TALER_KYCLOGIC_ProviderDetails *pd;
    310 
    311   /**
    312    * Connection we are handling.
    313    */
    314   struct MHD_Connection *connection;
    315 
    316   /**
    317    * Verification ID from the service.
    318    */
    319   char *inquiry_id;
    320 
    321   /**
    322    * Account ID from the service.
    323    */
    324   char *account_id;
    325 
    326   /**
    327    * URL of the cURL request.
    328    */
    329   char *url;
    330 
    331   /**
    332    * Handle for the request.
    333    */
    334   struct GNUNET_CURL_Job *job;
    335 
    336   /**
    337    * Response to return asynchronously.
    338    */
    339   struct MHD_Response *resp;
    340 
    341   /**
    342    * ID of the template the webhook is about,
    343    * according to the service.
    344    */
    345   const char *template_id;
    346 
    347   /**
    348    * Handle to an external process that converts the
    349    * Persona response to our internal format.
    350    */
    351   struct TALER_JSON_ExternalConversion *ec;
    352 
    353   /**
    354    * Our account ID.
    355    */
    356   struct TALER_NormalizedPaytoHashP h_payto;
    357 
    358   /**
    359    * UUID being checked.
    360    */
    361   uint64_t process_row;
    362 
    363   /**
    364    * HTTP status returned by Persona to us.
    365    */
    366   unsigned int persona_http_status;
    367 
    368   /**
    369    * HTTP response code to return asynchronously.
    370    */
    371   unsigned int response_code;
    372 
    373   /**
    374    * True if @e h_payto is for a wallet.
    375    */
    376   bool is_wallet;
    377 };
    378 
    379 
    380 /**
    381  * Release configuration resources previously loaded
    382  *
    383  * @param[in] pd configuration to release
    384  */
    385 static void
    386 persona_unload_configuration (struct TALER_KYCLOGIC_ProviderDetails *pd)
    387 {
    388   curl_slist_free_all (pd->slist);
    389   GNUNET_free (pd->auth_token);
    390   GNUNET_free (pd->template_id);
    391   GNUNET_free (pd->subdomain);
    392   GNUNET_free (pd->conversion_binary);
    393   GNUNET_free (pd->salt);
    394   GNUNET_free (pd->section);
    395   GNUNET_free (pd->provider_name);
    396   GNUNET_free (pd->post_kyc_redirect_url);
    397   GNUNET_free (pd);
    398 }
    399 
    400 
    401 /**
    402  * Load the configuration of the KYC provider.
    403  *
    404  * @param cls closure
    405  * @param provider_section_name configuration section to parse
    406  * @return NULL if configuration is invalid
    407  */
    408 static struct TALER_KYCLOGIC_ProviderDetails *
    409 persona_load_configuration (void *cls,
    410                             const char *provider_section_name)
    411 {
    412   struct PluginState *ps = cls;
    413   struct TALER_KYCLOGIC_ProviderDetails *pd;
    414 
    415   pd = GNUNET_new (struct TALER_KYCLOGIC_ProviderDetails);
    416   pd->ps = ps;
    417   pd->section = GNUNET_strdup (provider_section_name);
    418   pd->provider_name
    419     = GNUNET_strdup (
    420         (0 == strncasecmp (provider_section_name,
    421                            "kyc-provider-",
    422                            strlen ("kyc-provider-")))
    423         ? &provider_section_name[strlen ("kyc-provider-")]
    424         : provider_section_name);
    425   if (GNUNET_OK !=
    426       GNUNET_CONFIGURATION_get_value_time (ps->cfg,
    427                                            provider_section_name,
    428                                            "KYC_PERSONA_VALIDITY",
    429                                            &pd->validity))
    430   {
    431     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    432                                provider_section_name,
    433                                "KYC_PERSONA_VALIDITY");
    434     persona_unload_configuration (pd);
    435     return NULL;
    436   }
    437   if (GNUNET_OK !=
    438       GNUNET_CONFIGURATION_get_value_string (ps->cfg,
    439                                              provider_section_name,
    440                                              "KYC_PERSONA_AUTH_TOKEN",
    441                                              &pd->auth_token))
    442   {
    443     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    444                                provider_section_name,
    445                                "KYC_PERSONA_AUTH_TOKEN");
    446     persona_unload_configuration (pd);
    447     return NULL;
    448   }
    449   if (GNUNET_OK !=
    450       GNUNET_CONFIGURATION_get_value_string (ps->cfg,
    451                                              provider_section_name,
    452                                              "KYC_PERSONA_SALT",
    453                                              &pd->salt))
    454   {
    455     uint32_t salt[8];
    456 
    457     GNUNET_CRYPTO_random_block (salt,
    458                                 sizeof (salt));
    459     pd->salt = GNUNET_STRINGS_data_to_string_alloc (salt,
    460                                                     sizeof (salt));
    461   }
    462   if (GNUNET_OK !=
    463       GNUNET_CONFIGURATION_get_value_string (ps->cfg,
    464                                              provider_section_name,
    465                                              "KYC_PERSONA_SUBDOMAIN",
    466                                              &pd->subdomain))
    467   {
    468     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    469                                provider_section_name,
    470                                "KYC_PERSONA_SUBDOMAIN");
    471     persona_unload_configuration (pd);
    472     return NULL;
    473   }
    474   if (GNUNET_OK !=
    475       GNUNET_CONFIGURATION_get_value_string (ps->cfg,
    476                                              provider_section_name,
    477                                              "KYC_PERSONA_CONVERTER_HELPER",
    478                                              &pd->conversion_binary))
    479   {
    480     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    481                                provider_section_name,
    482                                "KYC_PERSONA_CONVERTER_HELPER");
    483     persona_unload_configuration (pd);
    484     return NULL;
    485   }
    486   if (GNUNET_OK !=
    487       GNUNET_CONFIGURATION_get_value_string (ps->cfg,
    488                                              provider_section_name,
    489                                              "KYC_PERSONA_POST_URL",
    490                                              &pd->post_kyc_redirect_url))
    491   {
    492     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    493                                provider_section_name,
    494                                "KYC_PERSONA_POST_URL");
    495     persona_unload_configuration (pd);
    496     return NULL;
    497   }
    498   if (GNUNET_OK !=
    499       GNUNET_CONFIGURATION_get_value_string (ps->cfg,
    500                                              provider_section_name,
    501                                              "KYC_PERSONA_TEMPLATE_ID",
    502                                              &pd->template_id))
    503   {
    504     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    505                                provider_section_name,
    506                                "KYC_PERSONA_TEMPLATE_ID");
    507     persona_unload_configuration (pd);
    508     return NULL;
    509   }
    510   {
    511     char *auth;
    512 
    513     GNUNET_asprintf (&auth,
    514                      "%s: Bearer %s",
    515                      MHD_HTTP_HEADER_AUTHORIZATION,
    516                      pd->auth_token);
    517     pd->slist = curl_slist_append (NULL,
    518                                    auth);
    519     GNUNET_free (auth);
    520     GNUNET_asprintf (&auth,
    521                      "%s: %s",
    522                      MHD_HTTP_HEADER_ACCEPT,
    523                      "application/json");
    524     pd->slist = curl_slist_append (pd->slist,
    525                                    auth);
    526     GNUNET_free (auth);
    527     pd->slist = curl_slist_append (pd->slist,
    528                                    "Persona-Version: "
    529                                    PERSONA_VERSION);
    530   }
    531   return pd;
    532 }
    533 
    534 
    535 /**
    536  * Cancel KYC check initiation.
    537  *
    538  * @param[in] ih handle of operation to cancel
    539  */
    540 static void
    541 persona_initiate_cancel (struct TALER_KYCLOGIC_InitiateHandle *ih)
    542 {
    543   if (NULL != ih->job)
    544   {
    545     GNUNET_CURL_job_cancel (ih->job);
    546     ih->job = NULL;
    547   }
    548   GNUNET_free (ih->url);
    549   TALER_curl_easy_post_finished (&ih->ctx);
    550   curl_slist_free_all (ih->slist);
    551   GNUNET_free (ih);
    552 }
    553 
    554 
    555 /**
    556  * Function called when we're done processing the
    557  * HTTP POST "/api/v1/inquiries" request.
    558  *
    559  * @param cls the `struct TALER_KYCLOGIC_InitiateHandle`
    560  * @param response_code HTTP response code, 0 on error
    561  * @param response parsed JSON result, NULL on error
    562  */
    563 static void
    564 handle_initiate_finished (void *cls,
    565                           long response_code,
    566                           const void *response)
    567 {
    568   struct TALER_KYCLOGIC_InitiateHandle *ih = cls;
    569   const struct TALER_KYCLOGIC_ProviderDetails *pd = ih->pd;
    570   const json_t *j = response;
    571   char *url;
    572   json_t *data;
    573   const char *type;
    574   const char *inquiry_id;
    575   const char *persona_account_id;
    576   const char *ename;
    577   unsigned int eline;
    578   struct GNUNET_JSON_Specification spec[] = {
    579     GNUNET_JSON_spec_string ("type",
    580                              &type),
    581     GNUNET_JSON_spec_string ("id",
    582                              &inquiry_id),
    583     GNUNET_JSON_spec_end ()
    584   };
    585 
    586   ih->job = NULL;
    587   switch (response_code)
    588   {
    589   case MHD_HTTP_CREATED:
    590     /* handled below */
    591     break;
    592   case MHD_HTTP_UNAUTHORIZED:
    593   case MHD_HTTP_FORBIDDEN:
    594     {
    595       const char *msg;
    596 
    597       msg = json_string_value (
    598         json_object_get (
    599           json_array_get (
    600             json_object_get (j,
    601                              "errors"),
    602             0),
    603           "title"));
    604 
    605       ih->cb (ih->cb_cls,
    606               TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED,
    607               NULL,
    608               NULL,
    609               NULL,
    610               msg);
    611       persona_initiate_cancel (ih);
    612       return;
    613     }
    614   case MHD_HTTP_NOT_FOUND:
    615   case MHD_HTTP_CONFLICT:
    616     {
    617       const char *msg;
    618 
    619       msg = json_string_value (
    620         json_object_get (
    621           json_array_get (
    622             json_object_get (j,
    623                              "errors"),
    624             0),
    625           "title"));
    626 
    627       ih->cb (ih->cb_cls,
    628               TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY,
    629               NULL,
    630               NULL,
    631               NULL,
    632               msg);
    633       persona_initiate_cancel (ih);
    634       return;
    635     }
    636   case MHD_HTTP_BAD_REQUEST:
    637   case MHD_HTTP_UNPROCESSABLE_CONTENT:
    638     {
    639       const char *msg;
    640 
    641       GNUNET_break (0);
    642 #if DEBUG
    643       json_dumpf (j,
    644                   stderr,
    645                   JSON_INDENT (2));
    646 #endif
    647       msg = json_string_value (
    648         json_object_get (
    649           json_array_get (
    650             json_object_get (j,
    651                              "errors"),
    652             0),
    653           "title"));
    654 
    655       ih->cb (ih->cb_cls,
    656               TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_BUG,
    657               NULL,
    658               NULL,
    659               NULL,
    660               msg);
    661       persona_initiate_cancel (ih);
    662       return;
    663     }
    664   case MHD_HTTP_TOO_MANY_REQUESTS:
    665     {
    666       const char *msg;
    667 
    668       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    669                   "Rate limiting requested:\n");
    670 #if DEBUG
    671       json_dumpf (j,
    672                   stderr,
    673                   JSON_INDENT (2));
    674 #endif
    675       msg = json_string_value (
    676         json_object_get (
    677           json_array_get (
    678             json_object_get (j,
    679                              "errors"),
    680             0),
    681           "title"));
    682       ih->cb (ih->cb_cls,
    683               TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED,
    684               NULL,
    685               NULL,
    686               NULL,
    687               msg);
    688       persona_initiate_cancel (ih);
    689       return;
    690     }
    691   default:
    692     {
    693       char *err;
    694 
    695       GNUNET_break_op (0);
    696 #if DEBUG
    697       json_dumpf (j,
    698                   stderr,
    699                   JSON_INDENT (2));
    700 #endif
    701       GNUNET_asprintf (&err,
    702                        "Unexpected HTTP status %u from Persona\n",
    703                        (unsigned int) response_code);
    704       ih->cb (ih->cb_cls,
    705               TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY,
    706               NULL,
    707               NULL,
    708               NULL,
    709               err);
    710       GNUNET_free (err);
    711       persona_initiate_cancel (ih);
    712       return;
    713     }
    714   }
    715   data = json_object_get (j,
    716                           "data");
    717   if (NULL == data)
    718   {
    719     GNUNET_break_op (0);
    720 #if DEBUG
    721     json_dumpf (j,
    722                 stderr,
    723                 JSON_INDENT (2));
    724 #endif
    725     ih->cb (ih->cb_cls,
    726             TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY,
    727             NULL,
    728             NULL,
    729             NULL,
    730             "'data' field missing in Persona response");
    731     persona_initiate_cancel (ih);
    732     return;
    733   }
    734 
    735   if (GNUNET_OK !=
    736       GNUNET_JSON_parse (data,
    737                          spec,
    738                          &ename,
    739                          &eline))
    740   {
    741     GNUNET_break_op (0);
    742 #if DEBUG
    743     json_dumpf (j,
    744                 stderr,
    745                 JSON_INDENT (2));
    746 #endif
    747     ih->cb (ih->cb_cls,
    748             TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY,
    749             NULL,
    750             NULL,
    751             NULL,
    752             ename);
    753     persona_initiate_cancel (ih);
    754     return;
    755   }
    756   persona_account_id
    757     = json_string_value (
    758         json_object_get (
    759           json_object_get (
    760             json_object_get (
    761               json_object_get (data,
    762                                "relationships"),
    763               "account"),
    764             "data"),
    765           "id"));
    766   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    767               "Starting inquiry %s for Persona account %s\n",
    768               inquiry_id,
    769               persona_account_id);
    770   {
    771     char *inquiry_id_encoded;
    772 
    773     inquiry_id_encoded = TALER_urlencode (inquiry_id);
    774     GNUNET_asprintf (&url,
    775                      "https://%s.withpersona.com/verify"
    776                      "?inquiry-id=%s",
    777                      pd->subdomain,
    778                      inquiry_id_encoded);
    779     GNUNET_free (inquiry_id_encoded);
    780   }
    781   ih->cb (ih->cb_cls,
    782           TALER_EC_NONE,
    783           url,
    784           persona_account_id,
    785           inquiry_id,
    786           NULL);
    787   GNUNET_free (url);
    788   persona_initiate_cancel (ih);
    789 }
    790 
    791 
    792 /**
    793  * Initiate KYC check.
    794  *
    795  * @param cls the @e cls of this struct with the plugin-specific state
    796  * @param pd provider configuration details
    797  * @param account_id which account to trigger process for
    798  * @param legitimization_uuid unique ID for the legitimization process
    799  * @param context additional contextual information for the legi process
    800  * @param cb function to call with the result
    801  * @param cb_cls closure for @a cb
    802  * @return handle to cancel operation early
    803  */
    804 static struct TALER_KYCLOGIC_InitiateHandle *
    805 persona_initiate (void *cls,
    806                   const struct TALER_KYCLOGIC_ProviderDetails *pd,
    807                   const struct TALER_NormalizedPaytoHashP *account_id,
    808                   uint64_t legitimization_uuid,
    809                   const json_t *context,
    810                   TALER_KYCLOGIC_InitiateCallback cb,
    811                   void *cb_cls)
    812 {
    813   struct PluginState *ps = cls;
    814   struct TALER_KYCLOGIC_InitiateHandle *ih;
    815   json_t *body;
    816   CURL *eh;
    817 
    818   (void) context;
    819   eh = curl_easy_init ();
    820   if (NULL == eh)
    821   {
    822     GNUNET_break (0);
    823     return NULL;
    824   }
    825   ih = GNUNET_new (struct TALER_KYCLOGIC_InitiateHandle);
    826   ih->legitimization_uuid = legitimization_uuid;
    827   ih->cb = cb;
    828   ih->cb_cls = cb_cls;
    829   ih->h_payto = *account_id;
    830   ih->pd = pd;
    831   GNUNET_asprintf (&ih->url,
    832                    "https://withpersona.com/api/v1/inquiries");
    833   {
    834     char *payto_s;
    835     char *proof_url;
    836     char ref_s[24];
    837 
    838     GNUNET_snprintf (ref_s,
    839                      sizeof (ref_s),
    840                      "%llu",
    841                      (unsigned long long) ih->legitimization_uuid);
    842     payto_s = GNUNET_STRINGS_data_to_string_alloc (&ih->h_payto,
    843                                                    sizeof (ih->h_payto));
    844     GNUNET_break ('/' ==
    845                   pd->ps->exchange_base_url[strlen (
    846                                               pd->ps->exchange_base_url) - 1]);
    847     GNUNET_asprintf (&proof_url,
    848                      "%skyc-proof/%s?state=%s",
    849                      pd->ps->exchange_base_url,
    850                      &pd->section[strlen ("kyc-provider-")],
    851                      payto_s);
    852     body = GNUNET_JSON_PACK (
    853       GNUNET_JSON_pack_object_steal (
    854         "data",
    855         GNUNET_JSON_PACK (
    856           GNUNET_JSON_pack_object_steal (
    857             "attributes",
    858             GNUNET_JSON_PACK (
    859               GNUNET_JSON_pack_string ("inquiry_template_id",
    860                                        pd->template_id),
    861               GNUNET_JSON_pack_string ("reference_id",
    862                                        ref_s),
    863               GNUNET_JSON_pack_string ("redirect_uri",
    864                                        proof_url)
    865               )))));
    866     GNUNET_assert (NULL != body);
    867     GNUNET_free (payto_s);
    868     GNUNET_free (proof_url);
    869   }
    870   GNUNET_break (CURLE_OK ==
    871                 curl_easy_setopt (eh,
    872                                   CURLOPT_VERBOSE,
    873                                   0));
    874   GNUNET_assert (CURLE_OK ==
    875                  curl_easy_setopt (eh,
    876                                    CURLOPT_MAXREDIRS,
    877                                    1L));
    878   GNUNET_break (CURLE_OK ==
    879                 curl_easy_setopt (eh,
    880                                   CURLOPT_URL,
    881                                   ih->url));
    882   ih->ctx.disable_compression = true;
    883   if (GNUNET_OK !=
    884       TALER_curl_easy_post (&ih->ctx,
    885                             eh,
    886                             body))
    887   {
    888     GNUNET_break (0);
    889     GNUNET_free (ih->url);
    890     GNUNET_free (ih);
    891     curl_easy_cleanup (eh);
    892     json_decref (body);
    893     return NULL;
    894   }
    895   json_decref (body);
    896   ih->job = GNUNET_CURL_job_add2 (ps->curl_ctx,
    897                                   eh,
    898                                   ih->ctx.headers,
    899                                   &handle_initiate_finished,
    900                                   ih);
    901   GNUNET_CURL_extend_headers (ih->job,
    902                               pd->slist);
    903   {
    904     char *ikh;
    905 
    906     GNUNET_asprintf (&ikh,
    907                      "Idempotency-Key: %llu-%s",
    908                      (unsigned long long) ih->legitimization_uuid,
    909                      pd->salt);
    910     ih->slist = curl_slist_append (NULL,
    911                                    ikh);
    912     GNUNET_free (ikh);
    913   }
    914   GNUNET_CURL_extend_headers (ih->job,
    915                               ih->slist);
    916   return ih;
    917 }
    918 
    919 
    920 /**
    921  * Cancel KYC proof.
    922  *
    923  * @param[in] ph handle of operation to cancel
    924  */
    925 static void
    926 persona_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph)
    927 {
    928   if (NULL != ph->job)
    929   {
    930     GNUNET_CURL_job_cancel (ph->job);
    931     ph->job = NULL;
    932   }
    933   if (NULL != ph->ec)
    934   {
    935     TALER_JSON_external_conversion_stop (ph->ec);
    936     ph->ec = NULL;
    937   }
    938   GNUNET_free (ph->url);
    939   GNUNET_free (ph->provider_user_id);
    940   GNUNET_free (ph->account_id);
    941   GNUNET_free (ph->inquiry_id);
    942   GNUNET_free (ph);
    943 }
    944 
    945 
    946 /**
    947  * Call @a ph callback with the operation result.
    948  *
    949  * @param ph proof handle to generate reply for
    950  * @param status status to return
    951  * @param account_id account to return
    952  * @param inquiry_id inquiry ID to supply, NULL if unknown
    953  * @param http_status HTTP status to use
    954  * @param template template to instantiate
    955  * @param[in] body body for the template to use (reference
    956  *         is consumed)
    957  */
    958 static void
    959 proof_generic_reply (struct TALER_KYCLOGIC_ProofHandle *ph,
    960                      enum TALER_KYCLOGIC_KycStatus status,
    961                      const char *account_id,
    962                      const char *inquiry_id,
    963                      unsigned int http_status,
    964                      const char *template,
    965                      json_t *body)
    966 {
    967   struct MHD_Response *resp;
    968   enum GNUNET_GenericReturnValue ret;
    969 
    970   /* This API is not usable for successful replies */
    971   GNUNET_assert (TALER_KYCLOGIC_STATUS_SUCCESS != status);
    972   ret = TALER_TEMPLATING_build (ph->connection,
    973                                 &http_status,
    974                                 template,
    975                                 NULL,
    976                                 NULL,
    977                                 body,
    978                                 &resp);
    979   json_decref (body);
    980   if (GNUNET_SYSERR == ret)
    981   {
    982     GNUNET_break (0);
    983     resp = NULL; /* good luck */
    984   }
    985   else
    986   {
    987     GNUNET_break (MHD_NO !=
    988                   MHD_add_response_header (resp,
    989                                            MHD_HTTP_HEADER_CONTENT_TYPE,
    990                                            "text/html"));
    991   }
    992   ph->cb (ph->cb_cls,
    993           status,
    994           ph->pd->provider_name,
    995           account_id,
    996           inquiry_id,
    997           GNUNET_TIME_UNIT_ZERO_ABS,
    998           NULL,
    999           http_status,
   1000           resp);
   1001 }
   1002 
   1003 
   1004 /**
   1005  * Call @a ph callback with HTTP error response.
   1006  *
   1007  * @param ph proof handle to generate reply for
   1008  * @param inquiry_id inquiry ID to supply
   1009  * @param http_status HTTP status to use
   1010  * @param template template to instantiate
   1011  * @param[in] body body for the template to use (reference
   1012  *         is consumed)
   1013  */
   1014 static void
   1015 proof_reply_error (struct TALER_KYCLOGIC_ProofHandle *ph,
   1016                    const char *inquiry_id,
   1017                    unsigned int http_status,
   1018                    const char *template,
   1019                    json_t *body)
   1020 {
   1021   proof_generic_reply (ph,
   1022                        TALER_KYCLOGIC_STATUS_PROVIDER_FAILED,
   1023                        NULL, /* user id */
   1024                        inquiry_id,
   1025                        http_status,
   1026                        template,
   1027                        body);
   1028 }
   1029 
   1030 
   1031 /**
   1032  * Return a response for the @a ph request indicating a
   1033  * protocol violation by the Persona server.
   1034  *
   1035  * @param[in,out] ph request we are processing
   1036  * @param response_code HTTP status returned by Persona
   1037  * @param inquiry_id ID of the inquiry this is about
   1038  * @param detail where the response was wrong
   1039  * @param data full response data to output
   1040  */
   1041 static void
   1042 return_invalid_response (struct TALER_KYCLOGIC_ProofHandle *ph,
   1043                          unsigned int response_code,
   1044                          const char *inquiry_id,
   1045                          const char *detail,
   1046                          const json_t *data)
   1047 {
   1048   proof_reply_error (
   1049     ph,
   1050     inquiry_id,
   1051     MHD_HTTP_BAD_GATEWAY,
   1052     "persona-invalid-response",
   1053     GNUNET_JSON_PACK (
   1054       GNUNET_JSON_pack_uint64 ("persona_http_status",
   1055                                response_code),
   1056       GNUNET_JSON_pack_allow_null (
   1057         GNUNET_JSON_pack_string ("persona_inquiry_id",
   1058                                  inquiry_id)),
   1059       TALER_JSON_pack_ec (
   1060         TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY),
   1061       GNUNET_JSON_pack_string ("detail",
   1062                                detail),
   1063       GNUNET_JSON_pack_allow_null (
   1064         GNUNET_JSON_pack_object_incref ("data",
   1065                                         (json_t *)
   1066                                         data))));
   1067 }
   1068 
   1069 
   1070 /**
   1071  * Start the external conversion helper.
   1072  *
   1073  * @param pd configuration details
   1074  * @param attr attributes to give to the helper
   1075  * @param cb function to call with the result
   1076  * @param cb_cls closure for @a cb
   1077  * @return handle for the helper
   1078  */
   1079 static struct TALER_JSON_ExternalConversion *
   1080 start_conversion (const struct TALER_KYCLOGIC_ProviderDetails *pd,
   1081                   const json_t *attr,
   1082                   TALER_JSON_JsonCallback cb,
   1083                   void *cb_cls)
   1084 {
   1085   const char *argv[] = {
   1086     pd->conversion_binary,
   1087     "-a",
   1088     pd->auth_token,
   1089     NULL,
   1090   };
   1091 
   1092   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
   1093               "Calling converter `%s' with JSON\n",
   1094               pd->conversion_binary);
   1095 #if DEBUG
   1096   json_dumpf (attr,
   1097               stderr,
   1098               JSON_INDENT (2));
   1099 #endif
   1100   return TALER_JSON_external_conversion_start (
   1101     attr,
   1102     cb,
   1103     cb_cls,
   1104     pd->conversion_binary,
   1105     argv);
   1106 }
   1107 
   1108 
   1109 /**
   1110  * Type of a callback that receives a JSON @a result.
   1111  *
   1112  * @param cls closure with a `struct TALER_KYCLOGIC_ProofHandle *`
   1113  * @param status_type how did the process die
   1114  * @param code termination status code from the process
   1115  * @param attr result some JSON result, NULL if we failed to get an JSON output
   1116  */
   1117 static void
   1118 proof_post_conversion_cb (void *cls,
   1119                           enum GNUNET_OS_ProcessStatusType status_type,
   1120                           unsigned long code,
   1121                           const json_t *attr)
   1122 {
   1123   struct TALER_KYCLOGIC_ProofHandle *ph = cls;
   1124   struct MHD_Response *resp;
   1125   struct GNUNET_TIME_Absolute expiration;
   1126 
   1127   ph->ec = NULL;
   1128   if ( (NULL == attr) ||
   1129        (GNUNET_OS_PROCESS_EXITED != status_type) ||
   1130        (0 != code) )
   1131   {
   1132     GNUNET_break_op (0);
   1133     return_invalid_response (ph,
   1134                              MHD_HTTP_OK,
   1135                              ph->inquiry_id,
   1136                              "converter",
   1137                              NULL);
   1138     persona_proof_cancel (ph);
   1139     return;
   1140   }
   1141   expiration = GNUNET_TIME_relative_to_absolute (ph->pd->validity);
   1142   resp = MHD_create_response_from_buffer_static (0,
   1143                                                  "");
   1144   GNUNET_break (MHD_YES ==
   1145                 MHD_add_response_header (resp,
   1146                                          MHD_HTTP_HEADER_LOCATION,
   1147                                          ph->pd->post_kyc_redirect_url));
   1148   TALER_MHD_add_global_headers (resp,
   1149                                 false);
   1150   ph->cb (ph->cb_cls,
   1151           TALER_KYCLOGIC_STATUS_SUCCESS,
   1152           ph->pd->provider_name,
   1153           ph->account_id,
   1154           ph->inquiry_id,
   1155           expiration,
   1156           attr,
   1157           MHD_HTTP_SEE_OTHER,
   1158           resp);
   1159   persona_proof_cancel (ph);
   1160 }
   1161 
   1162 
   1163 /**
   1164  * Function called when we're done processing the
   1165  * HTTP "/api/v1/inquiries/{inquiry-id}" request.
   1166  *
   1167  * @param cls the `struct TALER_KYCLOGIC_InitiateHandle`
   1168  * @param response_code HTTP response code, 0 on error
   1169  * @param response parsed JSON result, NULL on error
   1170  */
   1171 static void
   1172 handle_proof_finished (void *cls,
   1173                        long response_code,
   1174                        const void *response)
   1175 {
   1176   struct TALER_KYCLOGIC_ProofHandle *ph = cls;
   1177   const json_t *j = response;
   1178   const json_t *data = json_object_get (j,
   1179                                         "data");
   1180 
   1181   ph->job = NULL;
   1182   switch (response_code)
   1183   {
   1184   case MHD_HTTP_OK:
   1185     {
   1186       const char *inquiry_id = NULL;
   1187       const char *account_id;
   1188       const char *type = NULL;
   1189       const json_t *attributes;
   1190       const json_t *relationships;
   1191       struct GNUNET_JSON_Specification spec[] = {
   1192         GNUNET_JSON_spec_string ("type",
   1193                                  &type),
   1194         GNUNET_JSON_spec_string ("id",
   1195                                  &inquiry_id),
   1196         GNUNET_JSON_spec_object_const ("attributes",
   1197                                        &attributes),
   1198         GNUNET_JSON_spec_object_const ("relationships",
   1199                                        &relationships),
   1200         GNUNET_JSON_spec_end ()
   1201       };
   1202 
   1203       if ( (NULL == data) ||
   1204            (GNUNET_OK !=
   1205             GNUNET_JSON_parse (data,
   1206                                spec,
   1207                                NULL, NULL)) ||
   1208            (0 != strcasecmp (type,
   1209                              "inquiry")) )
   1210       {
   1211         GNUNET_break_op (0);
   1212         return_invalid_response (ph,
   1213                                  response_code,
   1214                                  inquiry_id,
   1215                                  "data",
   1216                                  data);
   1217         break;
   1218       }
   1219 
   1220       {
   1221         const char *status; /* "completed", what else? */
   1222         const char *reference_id; /* or legitimization number */
   1223         const char *expired_at = NULL; /* often 'null' format: "2022-08-18T10:14:26.000Z" */
   1224         struct GNUNET_JSON_Specification ispec[] = {
   1225           GNUNET_JSON_spec_string ("status",
   1226                                    &status),
   1227           GNUNET_JSON_spec_string ("reference-id",
   1228                                    &reference_id),
   1229           GNUNET_JSON_spec_mark_optional (
   1230             GNUNET_JSON_spec_string ("expired-at",
   1231                                      &expired_at),
   1232             NULL),
   1233           GNUNET_JSON_spec_end ()
   1234         };
   1235 
   1236         if (GNUNET_OK !=
   1237             GNUNET_JSON_parse (attributes,
   1238                                ispec,
   1239                                NULL, NULL))
   1240         {
   1241           GNUNET_break_op (0);
   1242           return_invalid_response (ph,
   1243                                    response_code,
   1244                                    inquiry_id,
   1245                                    "data-attributes",
   1246                                    data);
   1247           break;
   1248         }
   1249         {
   1250           unsigned long long idr;
   1251           char dummy;
   1252 
   1253           if ( (1 != sscanf (reference_id,
   1254                              "%llu%c",
   1255                              &idr,
   1256                              &dummy)) ||
   1257                (idr != ph->process_row) )
   1258           {
   1259             GNUNET_break_op (0);
   1260             return_invalid_response (ph,
   1261                                      response_code,
   1262                                      inquiry_id,
   1263                                      "data-attributes-reference_id",
   1264                                      data);
   1265             break;
   1266           }
   1267         }
   1268 
   1269         if (0 != strcmp (inquiry_id,
   1270                          ph->inquiry_id))
   1271         {
   1272           GNUNET_break_op (0);
   1273           return_invalid_response (ph,
   1274                                    response_code,
   1275                                    inquiry_id,
   1276                                    "data-id",
   1277                                    data);
   1278           break;
   1279         }
   1280 
   1281         account_id = json_string_value (
   1282           json_object_get (
   1283             json_object_get (
   1284               json_object_get (
   1285                 relationships,
   1286                 "account"),
   1287               "data"),
   1288             "id"));
   1289 
   1290         if (0 != strcasecmp (status,
   1291                              "completed"))
   1292         {
   1293           proof_generic_reply (
   1294             ph,
   1295             TALER_KYCLOGIC_STATUS_FAILED,
   1296             account_id,
   1297             inquiry_id,
   1298             MHD_HTTP_OK,
   1299             "persona-kyc-failed",
   1300             GNUNET_JSON_PACK (
   1301               GNUNET_JSON_pack_uint64 ("persona_http_status",
   1302                                        response_code),
   1303               GNUNET_JSON_pack_string ("persona_inquiry_id",
   1304                                        inquiry_id),
   1305               GNUNET_JSON_pack_allow_null (
   1306                 GNUNET_JSON_pack_object_incref ("data",
   1307                                                 (json_t *)
   1308                                                 data))));
   1309           break;
   1310         }
   1311 
   1312         if (NULL == account_id)
   1313         {
   1314           GNUNET_break_op (0);
   1315           return_invalid_response (ph,
   1316                                    response_code,
   1317                                    inquiry_id,
   1318                                    "data-relationships-account-data-id",
   1319                                    data);
   1320           break;
   1321         }
   1322         ph->account_id = GNUNET_strdup (account_id);
   1323         ph->ec = start_conversion (ph->pd,
   1324                                    j,
   1325                                    &proof_post_conversion_cb,
   1326                                    ph);
   1327         if (NULL == ph->ec)
   1328         {
   1329           GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1330                       "Failed to start Persona conversion helper\n");
   1331           proof_reply_error (
   1332             ph,
   1333             ph->inquiry_id,
   1334             MHD_HTTP_BAD_GATEWAY,
   1335             "persona-logic-failure",
   1336             GNUNET_JSON_PACK (
   1337               TALER_JSON_pack_ec (
   1338                 TALER_EC_EXCHANGE_GENERIC_KYC_CONVERTER_FAILED)));
   1339           break;
   1340         }
   1341       }
   1342       return; /* continued in proof_post_conversion_cb */
   1343     }
   1344   case MHD_HTTP_BAD_REQUEST:
   1345   case MHD_HTTP_NOT_FOUND:
   1346   case MHD_HTTP_CONFLICT:
   1347   case MHD_HTTP_UNPROCESSABLE_CONTENT:
   1348     /* These are errors with this code */
   1349     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1350                 "PERSONA failed with response %u:\n",
   1351                 (unsigned int) response_code);
   1352 #if DEBUG
   1353     json_dumpf (j,
   1354                 stderr,
   1355                 JSON_INDENT (2));
   1356 #endif
   1357     proof_reply_error (
   1358       ph,
   1359       ph->inquiry_id,
   1360       MHD_HTTP_BAD_GATEWAY,
   1361       "persona-logic-failure",
   1362       GNUNET_JSON_PACK (
   1363         GNUNET_JSON_pack_uint64 ("persona_http_status",
   1364                                  response_code),
   1365         TALER_JSON_pack_ec (
   1366           TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY),
   1367 
   1368         GNUNET_JSON_pack_allow_null (
   1369           GNUNET_JSON_pack_object_incref ("data",
   1370                                           (json_t *)
   1371                                           data))));
   1372     break;
   1373   case MHD_HTTP_UNAUTHORIZED:
   1374     /* These are failures of the exchange operator */
   1375     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1376                 "Refused access with HTTP status code %u\n",
   1377                 (unsigned int) response_code);
   1378     proof_reply_error (
   1379       ph,
   1380       ph->inquiry_id,
   1381       MHD_HTTP_BAD_GATEWAY,
   1382       "persona-exchange-unauthorized",
   1383       GNUNET_JSON_PACK (
   1384         GNUNET_JSON_pack_uint64 ("persona_http_status",
   1385                                  response_code),
   1386         TALER_JSON_pack_ec (
   1387           TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED),
   1388         GNUNET_JSON_pack_allow_null (
   1389           GNUNET_JSON_pack_object_incref ("data",
   1390                                           (json_t *)
   1391                                           data))));
   1392     break;
   1393   case MHD_HTTP_PAYMENT_REQUIRED:
   1394     /* These are failures of the exchange operator */
   1395     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1396                 "Refused access with HTTP status code %u\n",
   1397                 (unsigned int) response_code);
   1398     proof_reply_error (
   1399       ph,
   1400       ph->inquiry_id,
   1401       MHD_HTTP_SERVICE_UNAVAILABLE,
   1402       "persona-exchange-unpaid",
   1403       GNUNET_JSON_PACK (
   1404         GNUNET_JSON_pack_uint64 ("persona_http_status",
   1405                                  response_code),
   1406         TALER_JSON_pack_ec (
   1407           TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED),
   1408         GNUNET_JSON_pack_allow_null (
   1409           GNUNET_JSON_pack_object_incref ("data",
   1410                                           (json_t *)
   1411                                           data))));
   1412     break;
   1413   case MHD_HTTP_REQUEST_TIMEOUT:
   1414     /* These are networking issues */
   1415     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1416                 "PERSONA failed with response %u:\n",
   1417                 (unsigned int) response_code);
   1418 #if DEBUG
   1419     json_dumpf (j,
   1420                 stderr,
   1421                 JSON_INDENT (2));
   1422 #endif
   1423     proof_reply_error (
   1424       ph,
   1425       ph->inquiry_id,
   1426       MHD_HTTP_GATEWAY_TIMEOUT,
   1427       "persona-network-timeout",
   1428       GNUNET_JSON_PACK (
   1429         GNUNET_JSON_pack_uint64 ("persona_http_status",
   1430                                  response_code),
   1431         TALER_JSON_pack_ec (
   1432           TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT),
   1433         GNUNET_JSON_pack_allow_null (
   1434           GNUNET_JSON_pack_object_incref ("data",
   1435                                           (json_t *)
   1436                                           data))));
   1437     break;
   1438   case MHD_HTTP_TOO_MANY_REQUESTS:
   1439     /* This is a load issue */
   1440     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1441                 "PERSONA failed with response %u:\n",
   1442                 (unsigned int) response_code);
   1443 #if DEBUG
   1444     json_dumpf (j,
   1445                 stderr,
   1446                 JSON_INDENT (2));
   1447 #endif
   1448     proof_reply_error (
   1449       ph,
   1450       ph->inquiry_id,
   1451       MHD_HTTP_SERVICE_UNAVAILABLE,
   1452       "persona-load-failure",
   1453       GNUNET_JSON_PACK (
   1454         GNUNET_JSON_pack_uint64 ("persona_http_status",
   1455                                  response_code),
   1456         TALER_JSON_pack_ec (
   1457           TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED),
   1458         GNUNET_JSON_pack_allow_null (
   1459           GNUNET_JSON_pack_object_incref ("data",
   1460                                           (json_t *)
   1461                                           data))));
   1462     break;
   1463   case MHD_HTTP_INTERNAL_SERVER_ERROR:
   1464     /* This is an issue with Persona */
   1465     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1466                 "PERSONA failed with response %u:\n",
   1467                 (unsigned int) response_code);
   1468 #if DEBUG
   1469     json_dumpf (j,
   1470                 stderr,
   1471                 JSON_INDENT (2));
   1472 #endif
   1473     proof_reply_error (
   1474       ph,
   1475       ph->inquiry_id,
   1476       MHD_HTTP_BAD_GATEWAY,
   1477       "persona-provider-failure",
   1478       GNUNET_JSON_PACK (
   1479         GNUNET_JSON_pack_uint64 ("persona_http_status",
   1480                                  response_code),
   1481         TALER_JSON_pack_ec (
   1482           TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_ERROR),
   1483         GNUNET_JSON_pack_allow_null (
   1484           GNUNET_JSON_pack_object_incref ("data",
   1485                                           (json_t *)
   1486                                           data))));
   1487     break;
   1488   default:
   1489     /* This is an issue with Persona */
   1490     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1491                 "PERSONA failed with response %u:\n",
   1492                 (unsigned int) response_code);
   1493 #if DEBUG
   1494     json_dumpf (j,
   1495                 stderr,
   1496                 JSON_INDENT (2));
   1497 #endif
   1498     proof_reply_error (
   1499       ph,
   1500       ph->inquiry_id,
   1501       MHD_HTTP_BAD_GATEWAY,
   1502       "persona-invalid-response",
   1503       GNUNET_JSON_PACK (
   1504         GNUNET_JSON_pack_uint64 ("persona_http_status",
   1505                                  response_code),
   1506         TALER_JSON_pack_ec (
   1507           TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY),
   1508         GNUNET_JSON_pack_allow_null (
   1509           GNUNET_JSON_pack_object_incref ("data",
   1510                                           (json_t *)
   1511                                           data))));
   1512     break;
   1513   }
   1514   persona_proof_cancel (ph);
   1515 }
   1516 
   1517 
   1518 /**
   1519  * Check KYC status and return final result to human.
   1520  *
   1521  * @param cls the @e cls of this struct with the plugin-specific state
   1522  * @param pd provider configuration details
   1523  * @param connection MHD connection object (for HTTP headers)
   1524  * @param account_id which account to trigger process for
   1525  * @param process_row row in the legitimization processes table the legitimization is for
   1526  * @param provider_user_id user ID (or NULL) the proof is for
   1527  * @param inquiry_id legitimization ID the proof is for
   1528  * @param cb function to call with the result
   1529  * @param cb_cls closure for @a cb
   1530  * @return handle to cancel operation early
   1531  */
   1532 static struct TALER_KYCLOGIC_ProofHandle *
   1533 persona_proof (void *cls,
   1534                const struct TALER_KYCLOGIC_ProviderDetails *pd,
   1535                struct MHD_Connection *connection,
   1536                const struct TALER_NormalizedPaytoHashP *account_id,
   1537                uint64_t process_row,
   1538                const char *provider_user_id,
   1539                const char *inquiry_id,
   1540                TALER_KYCLOGIC_ProofCallback cb,
   1541                void *cb_cls)
   1542 {
   1543   struct PluginState *ps = cls;
   1544   struct TALER_KYCLOGIC_ProofHandle *ph;
   1545   CURL *eh;
   1546 
   1547   eh = curl_easy_init ();
   1548   if (NULL == eh)
   1549   {
   1550     GNUNET_break (0);
   1551     return NULL;
   1552   }
   1553   ph = GNUNET_new (struct TALER_KYCLOGIC_ProofHandle);
   1554   ph->ps = ps;
   1555   ph->pd = pd;
   1556   ph->cb = cb;
   1557   ph->cb_cls = cb_cls;
   1558   ph->connection = connection;
   1559   ph->process_row = process_row;
   1560   ph->h_payto = *account_id;
   1561   /* Note: we do not expect this to be non-NULL */
   1562   if (NULL != provider_user_id)
   1563     ph->provider_user_id = GNUNET_strdup (provider_user_id);
   1564   if (NULL != inquiry_id)
   1565     ph->inquiry_id = GNUNET_strdup (inquiry_id);
   1566   {
   1567     char *inquiry_id_encoded;
   1568 
   1569     inquiry_id_encoded = TALER_urlencode (NULL != inquiry_id
   1570                                           ? inquiry_id
   1571                                           : "");
   1572     GNUNET_asprintf (&ph->url,
   1573                      "https://withpersona.com/api/v1/inquiries/%s",
   1574                      inquiry_id_encoded);
   1575     GNUNET_free (inquiry_id_encoded);
   1576   }
   1577   GNUNET_break (CURLE_OK ==
   1578                 curl_easy_setopt (eh,
   1579                                   CURLOPT_VERBOSE,
   1580                                   0));
   1581   GNUNET_assert (CURLE_OK ==
   1582                  curl_easy_setopt (eh,
   1583                                    CURLOPT_MAXREDIRS,
   1584                                    1L));
   1585   GNUNET_break (CURLE_OK ==
   1586                 curl_easy_setopt (eh,
   1587                                   CURLOPT_URL,
   1588                                   ph->url));
   1589   ph->job = GNUNET_CURL_job_add2 (ps->curl_ctx,
   1590                                   eh,
   1591                                   pd->slist,
   1592                                   &handle_proof_finished,
   1593                                   ph);
   1594   return ph;
   1595 }
   1596 
   1597 
   1598 /**
   1599  * Cancel KYC webhook execution.
   1600  *
   1601  * @param[in] wh handle of operation to cancel
   1602  */
   1603 static void
   1604 persona_webhook_cancel (struct TALER_KYCLOGIC_WebhookHandle *wh)
   1605 {
   1606   if (NULL != wh->task)
   1607   {
   1608     GNUNET_SCHEDULER_cancel (wh->task);
   1609     wh->task = NULL;
   1610   }
   1611   if (NULL != wh->job)
   1612   {
   1613     GNUNET_CURL_job_cancel (wh->job);
   1614     wh->job = NULL;
   1615   }
   1616   if (NULL != wh->ec)
   1617   {
   1618     TALER_JSON_external_conversion_stop (wh->ec);
   1619     wh->ec = NULL;
   1620   }
   1621   GNUNET_free (wh->account_id);
   1622   GNUNET_free (wh->inquiry_id);
   1623   GNUNET_free (wh->url);
   1624   GNUNET_free (wh);
   1625 }
   1626 
   1627 
   1628 /**
   1629  * Call @a wh callback with the operation result.
   1630  *
   1631  * @param wh proof handle to generate reply for
   1632  * @param status status to return
   1633  * @param account_id account to return
   1634  * @param inquiry_id inquiry ID to supply, NULL if unknown
   1635  * @param attr KYC attribute data for the client
   1636  * @param http_status HTTP status to use
   1637  */
   1638 static void
   1639 webhook_generic_reply (struct TALER_KYCLOGIC_WebhookHandle *wh,
   1640                        enum TALER_KYCLOGIC_KycStatus status,
   1641                        const char *account_id,
   1642                        const char *inquiry_id,
   1643                        const json_t *attr,
   1644                        unsigned int http_status)
   1645 {
   1646   struct MHD_Response *resp;
   1647   struct GNUNET_TIME_Absolute expiration;
   1648 
   1649   if (TALER_KYCLOGIC_STATUS_SUCCESS == status)
   1650     expiration = GNUNET_TIME_relative_to_absolute (wh->pd->validity);
   1651   else
   1652     expiration = GNUNET_TIME_UNIT_ZERO_ABS;
   1653   resp = MHD_create_response_from_buffer_static (0,
   1654                                                  "");
   1655   TALER_MHD_add_global_headers (resp,
   1656                                 true);
   1657   wh->cb (wh->cb_cls,
   1658           wh->process_row,
   1659           &wh->h_payto,
   1660           wh->is_wallet,
   1661           wh->pd->provider_name,
   1662           account_id,
   1663           inquiry_id,
   1664           status,
   1665           expiration,
   1666           attr,
   1667           http_status,
   1668           resp);
   1669 }
   1670 
   1671 
   1672 /**
   1673  * Call @a wh callback with HTTP error response.
   1674  *
   1675  * @param wh proof handle to generate reply for
   1676  * @param inquiry_id inquiry ID to supply, NULL if unknown
   1677  * @param http_status HTTP status to use
   1678  */
   1679 static void
   1680 webhook_reply_error (struct TALER_KYCLOGIC_WebhookHandle *wh,
   1681                      const char *inquiry_id,
   1682                      unsigned int http_status)
   1683 {
   1684   webhook_generic_reply (wh,
   1685                          TALER_KYCLOGIC_STATUS_PROVIDER_FAILED,
   1686                          NULL, /* user id */
   1687                          inquiry_id,
   1688                          NULL, /* attributes */
   1689                          http_status);
   1690 }
   1691 
   1692 
   1693 /**
   1694  * Type of a callback that receives a JSON @a result.
   1695  *
   1696  * @param cls closure with a `struct TALER_KYCLOGIC_WebhookHandle *`
   1697  * @param status_type how did the process die
   1698  * @param code termination status code from the process
   1699  * @param attr some JSON result, NULL if we failed to get an JSON output
   1700  */
   1701 static void
   1702 webhook_post_conversion_cb (void *cls,
   1703                             enum GNUNET_OS_ProcessStatusType status_type,
   1704                             unsigned long code,
   1705                             const json_t *attr)
   1706 {
   1707   struct TALER_KYCLOGIC_WebhookHandle *wh = cls;
   1708 
   1709   wh->ec = NULL;
   1710   if ( (GNUNET_OS_PROCESS_EXITED != status_type) ||
   1711        (0 != code) ||
   1712        (! json_is_string (json_object_get (attr,
   1713                                            "FORM_ID"))) )
   1714   {
   1715     struct MHD_Response *resp;
   1716 
   1717     /* Failure in our helper */
   1718     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
   1719                 "Converter died with status %d/%d or "
   1720                 "mandatory FORM_ID not set in result\n",
   1721                 (int) status_type,
   1722                 (int) code);
   1723 #if DEBUG
   1724     json_dumpf (attr,
   1725                 stderr,
   1726                 JSON_INDENT (2));
   1727 #endif
   1728     resp = TALER_MHD_MAKE_JSON_PACK (
   1729       GNUNET_JSON_pack_uint64 ("persona_http_status",
   1730                                wh->persona_http_status),
   1731       GNUNET_JSON_pack_object_incref ("persona_body",
   1732                                       (json_t *) attr));
   1733     wh->cb (wh->cb_cls,
   1734             wh->process_row,
   1735             &wh->h_payto,
   1736             wh->is_wallet,
   1737             wh->pd->provider_name,
   1738             NULL,
   1739             wh->inquiry_id,
   1740             TALER_KYCLOGIC_STATUS_PROVIDER_FAILED,
   1741             GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */
   1742             NULL,
   1743             MHD_HTTP_BAD_GATEWAY,
   1744             resp);
   1745     persona_webhook_cancel (wh);
   1746     return;
   1747   }
   1748 
   1749   webhook_generic_reply (wh,
   1750                          TALER_KYCLOGIC_STATUS_SUCCESS,
   1751                          wh->account_id,
   1752                          wh->inquiry_id,
   1753                          attr,
   1754                          MHD_HTTP_OK);
   1755 }
   1756 
   1757 
   1758 /**
   1759  * Function called when we're done processing the
   1760  * HTTP "/api/v1/inquiries/{inquiry_id}" request.
   1761  *
   1762  * @param cls the `struct TALER_KYCLOGIC_WebhookHandle`
   1763  * @param response_code HTTP response code, 0 on error
   1764  * @param response parsed JSON result, NULL on error
   1765  */
   1766 static void
   1767 handle_webhook_finished (void *cls,
   1768                          long response_code,
   1769                          const void *response)
   1770 {
   1771   struct TALER_KYCLOGIC_WebhookHandle *wh = cls;
   1772   const json_t *j = response;
   1773   const json_t *data = json_object_get (j,
   1774                                         "data");
   1775 
   1776   wh->job = NULL;
   1777   wh->persona_http_status = response_code;
   1778   switch (response_code)
   1779   {
   1780   case MHD_HTTP_OK:
   1781     {
   1782       const char *inquiry_id = NULL;
   1783       const char *account_id;
   1784       const char *type = NULL;
   1785       const json_t *attributes;
   1786       const json_t *relationships;
   1787       struct GNUNET_JSON_Specification spec[] = {
   1788         GNUNET_JSON_spec_string ("type",
   1789                                  &type),
   1790         GNUNET_JSON_spec_string ("id",
   1791                                  &inquiry_id),
   1792         GNUNET_JSON_spec_object_const ("attributes",
   1793                                        &attributes),
   1794         GNUNET_JSON_spec_object_const ("relationships",
   1795                                        &relationships),
   1796         GNUNET_JSON_spec_end ()
   1797       };
   1798 
   1799       if ( (NULL == data) ||
   1800            (GNUNET_OK !=
   1801             GNUNET_JSON_parse (data,
   1802                                spec,
   1803                                NULL, NULL)) ||
   1804            (0 != strcasecmp (type,
   1805                              "inquiry")) )
   1806       {
   1807         GNUNET_break_op (0);
   1808 #if DEBUG
   1809         json_dumpf (j,
   1810                     stderr,
   1811                     JSON_INDENT (2));
   1812 #endif
   1813         webhook_reply_error (wh,
   1814                              inquiry_id,
   1815                              MHD_HTTP_BAD_GATEWAY);
   1816         break;
   1817       }
   1818 
   1819       {
   1820         const char *status; /* "completed", what else? */
   1821         const char *reference_id; /* or legitimization number */
   1822         const char *expired_at = NULL; /* often 'null' format: "2022-08-18T10:14:26.000Z" */
   1823         struct GNUNET_JSON_Specification ispec[] = {
   1824           GNUNET_JSON_spec_string ("status",
   1825                                    &status),
   1826           GNUNET_JSON_spec_string ("reference-id",
   1827                                    &reference_id),
   1828           GNUNET_JSON_spec_mark_optional (
   1829             GNUNET_JSON_spec_string ("expired-at",
   1830                                      &expired_at),
   1831             NULL),
   1832           GNUNET_JSON_spec_end ()
   1833         };
   1834 
   1835         if (GNUNET_OK !=
   1836             GNUNET_JSON_parse (attributes,
   1837                                ispec,
   1838                                NULL, NULL))
   1839         {
   1840           GNUNET_break_op (0);
   1841 #if DEBUG
   1842           json_dumpf (j,
   1843                       stderr,
   1844                       JSON_INDENT (2));
   1845 #endif
   1846           webhook_reply_error (wh,
   1847                                inquiry_id,
   1848                                MHD_HTTP_BAD_GATEWAY);
   1849           break;
   1850         }
   1851         {
   1852           unsigned long long idr;
   1853           char dummy;
   1854 
   1855           if ( (1 != sscanf (reference_id,
   1856                              "%llu%c",
   1857                              &idr,
   1858                              &dummy)) ||
   1859                (idr != wh->process_row) )
   1860           {
   1861             GNUNET_break_op (0);
   1862             webhook_reply_error (wh,
   1863                                  inquiry_id,
   1864                                  MHD_HTTP_BAD_GATEWAY);
   1865             break;
   1866           }
   1867         }
   1868 
   1869         if (0 != strcmp (inquiry_id,
   1870                          wh->inquiry_id))
   1871         {
   1872           GNUNET_break_op (0);
   1873           webhook_reply_error (wh,
   1874                                inquiry_id,
   1875                                MHD_HTTP_BAD_GATEWAY);
   1876           break;
   1877         }
   1878 
   1879         account_id = json_string_value (
   1880           json_object_get (
   1881             json_object_get (
   1882               json_object_get (
   1883                 relationships,
   1884                 "account"),
   1885               "data"),
   1886             "id"));
   1887 
   1888         if (0 != strcasecmp (status,
   1889                              "completed"))
   1890         {
   1891           webhook_generic_reply (wh,
   1892                                  TALER_KYCLOGIC_STATUS_FAILED,
   1893                                  account_id,
   1894                                  inquiry_id,
   1895                                  NULL,
   1896                                  MHD_HTTP_OK);
   1897           break;
   1898         }
   1899 
   1900         if (NULL == account_id)
   1901         {
   1902           GNUNET_break_op (0);
   1903 #if DEBUG
   1904           json_dumpf (data,
   1905                       stderr,
   1906                       JSON_INDENT (2));
   1907 #endif
   1908           webhook_reply_error (wh,
   1909                                inquiry_id,
   1910                                MHD_HTTP_BAD_GATEWAY);
   1911           break;
   1912         }
   1913         wh->account_id = GNUNET_strdup (account_id);
   1914         wh->ec = start_conversion (wh->pd,
   1915                                    j,
   1916                                    &webhook_post_conversion_cb,
   1917                                    wh);
   1918         if (NULL == wh->ec)
   1919         {
   1920           GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1921                       "Failed to start Persona conversion helper\n");
   1922           webhook_reply_error (wh,
   1923                                inquiry_id,
   1924                                MHD_HTTP_INTERNAL_SERVER_ERROR);
   1925           break;
   1926         }
   1927       }
   1928       return; /* continued in webhook_post_conversion_cb */
   1929     }
   1930   case MHD_HTTP_BAD_REQUEST:
   1931   case MHD_HTTP_NOT_FOUND:
   1932   case MHD_HTTP_CONFLICT:
   1933   case MHD_HTTP_UNPROCESSABLE_CONTENT:
   1934     /* These are errors with this code */
   1935     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1936                 "PERSONA failed with response %u:\n",
   1937                 (unsigned int) response_code);
   1938 #if DEBUG
   1939     json_dumpf (j,
   1940                 stderr,
   1941                 JSON_INDENT (2));
   1942 #endif
   1943     webhook_reply_error (wh,
   1944                          wh->inquiry_id,
   1945                          MHD_HTTP_BAD_GATEWAY);
   1946     break;
   1947   case MHD_HTTP_UNAUTHORIZED:
   1948     /* These are failures of the exchange operator */
   1949     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1950                 "Refused access with HTTP status code %u\n",
   1951                 (unsigned int) response_code);
   1952     webhook_reply_error (wh,
   1953                          wh->inquiry_id,
   1954                          MHD_HTTP_INTERNAL_SERVER_ERROR);
   1955     break;
   1956   case MHD_HTTP_PAYMENT_REQUIRED:
   1957     /* These are failures of the exchange operator */
   1958     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1959                 "Refused access with HTTP status code %u\n",
   1960                 (unsigned int) response_code);
   1961 
   1962     webhook_reply_error (wh,
   1963                          wh->inquiry_id,
   1964                          MHD_HTTP_INTERNAL_SERVER_ERROR);
   1965     break;
   1966   case MHD_HTTP_REQUEST_TIMEOUT:
   1967     /* These are networking issues */
   1968     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1969                 "PERSONA failed with response %u:\n",
   1970                 (unsigned int) response_code);
   1971 #if DEBUG
   1972     json_dumpf (j,
   1973                 stderr,
   1974                 JSON_INDENT (2));
   1975 #endif
   1976     webhook_reply_error (wh,
   1977                          wh->inquiry_id,
   1978                          MHD_HTTP_GATEWAY_TIMEOUT);
   1979     break;
   1980   case MHD_HTTP_TOO_MANY_REQUESTS:
   1981     /* This is a load issue */
   1982     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1983                 "PERSONA failed with response %u:\n",
   1984                 (unsigned int) response_code);
   1985 #if DEBUG
   1986     json_dumpf (j,
   1987                 stderr,
   1988                 JSON_INDENT (2));
   1989 #endif
   1990     webhook_reply_error (wh,
   1991                          wh->inquiry_id,
   1992                          MHD_HTTP_SERVICE_UNAVAILABLE);
   1993     break;
   1994   case MHD_HTTP_INTERNAL_SERVER_ERROR:
   1995     /* This is an issue with Persona */
   1996     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1997                 "PERSONA failed with response %u:\n",
   1998                 (unsigned int) response_code);
   1999 #if DEBUG
   2000     json_dumpf (j,
   2001                 stderr,
   2002                 JSON_INDENT (2));
   2003 #endif
   2004     webhook_reply_error (wh,
   2005                          wh->inquiry_id,
   2006                          MHD_HTTP_BAD_GATEWAY);
   2007     break;
   2008   default:
   2009     /* This is an issue with Persona */
   2010     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   2011                 "PERSONA failed with response %u:\n",
   2012                 (unsigned int) response_code);
   2013 #if DEBUG
   2014     json_dumpf (j,
   2015                 stderr,
   2016                 JSON_INDENT (2));
   2017 #endif
   2018     webhook_reply_error (wh,
   2019                          wh->inquiry_id,
   2020                          MHD_HTTP_BAD_GATEWAY);
   2021     break;
   2022   }
   2023 
   2024   persona_webhook_cancel (wh);
   2025 }
   2026 
   2027 
   2028 /**
   2029  * Asynchronously return a reply for the webhook.
   2030  *
   2031  * @param cls a `struct TALER_KYCLOGIC_WebhookHandle *`
   2032  */
   2033 static void
   2034 async_webhook_reply (void *cls)
   2035 {
   2036   struct TALER_KYCLOGIC_WebhookHandle *wh = cls;
   2037 
   2038   wh->task = NULL;
   2039   wh->cb (wh->cb_cls,
   2040           wh->process_row,
   2041           (0 == wh->process_row)
   2042           ? NULL
   2043           : &wh->h_payto,
   2044           wh->is_wallet,
   2045           (NULL != wh->pd)
   2046           ? wh->pd->provider_name
   2047           : "<unknown>",
   2048           NULL,
   2049           wh->inquiry_id, /* provider legi ID */
   2050           TALER_KYCLOGIC_STATUS_PROVIDER_FAILED,
   2051           GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */
   2052           NULL,
   2053           wh->response_code,
   2054           wh->resp);
   2055   persona_webhook_cancel (wh);
   2056 }
   2057 
   2058 
   2059 /**
   2060  * Function called with the provider details and
   2061  * associated plugin closures for matching logics.
   2062  *
   2063  * @param cls closure
   2064  * @param pd provider details of a matching logic
   2065  * @param plugin_cls closure of the plugin
   2066  * @return #GNUNET_OK to continue to iterate
   2067  */
   2068 static enum GNUNET_GenericReturnValue
   2069 locate_details_cb (
   2070   void *cls,
   2071   const struct TALER_KYCLOGIC_ProviderDetails *pd,
   2072   void *plugin_cls)
   2073 {
   2074   struct TALER_KYCLOGIC_WebhookHandle *wh = cls;
   2075 
   2076   /* This type-checks 'pd' */
   2077   GNUNET_assert (plugin_cls == wh->ps);
   2078   if (0 == strcmp (pd->template_id,
   2079                    wh->template_id))
   2080   {
   2081     wh->pd = pd;
   2082     return GNUNET_NO;
   2083   }
   2084   return GNUNET_OK;
   2085 }
   2086 
   2087 
   2088 /**
   2089  * Check KYC status and return result for Webhook.  We do NOT implement the
   2090  * authentication check proposed by the PERSONA documentation, as it would
   2091  * allow an attacker who learns the access token to easily bypass the KYC
   2092  * checks. Instead, we insist on explicitly requesting the KYC status from the
   2093  * provider (at least on success).
   2094  *
   2095  * @param cls the @e cls of this struct with the plugin-specific state
   2096  * @param pd provider configuration details
   2097  * @param plc callback to lookup accounts with
   2098  * @param plc_cls closure for @a plc
   2099  * @param http_method HTTP method used for the webhook
   2100  * @param url_path rest of the URL after `/kyc-webhook/`
   2101  * @param connection MHD connection object (for HTTP headers)
   2102  * @param body HTTP request body
   2103  * @param cb function to call with the result
   2104  * @param cb_cls closure for @a cb
   2105  * @return handle to cancel operation early
   2106  */
   2107 static struct TALER_KYCLOGIC_WebhookHandle *
   2108 persona_webhook (void *cls,
   2109                  const struct TALER_KYCLOGIC_ProviderDetails *pd,
   2110                  TALER_KYCLOGIC_ProviderLookupCallback plc,
   2111                  void *plc_cls,
   2112                  const char *http_method,
   2113                  const char *const url_path[],
   2114                  struct MHD_Connection *connection,
   2115                  const json_t *body,
   2116                  TALER_KYCLOGIC_WebhookCallback cb,
   2117                  void *cb_cls)
   2118 {
   2119   struct PluginState *ps = cls;
   2120   struct TALER_KYCLOGIC_WebhookHandle *wh;
   2121   CURL *eh;
   2122   enum GNUNET_DB_QueryStatus qs;
   2123   const char *persona_inquiry_id;
   2124   const char *auth_header;
   2125   bool bad_auth = false;
   2126 
   2127   /* Persona webhooks are expected by logic, not by template */
   2128   GNUNET_break_op (NULL == pd);
   2129   wh = GNUNET_new (struct TALER_KYCLOGIC_WebhookHandle);
   2130   wh->cb = cb;
   2131   wh->cb_cls = cb_cls;
   2132   wh->ps = ps;
   2133   wh->connection = connection;
   2134   wh->pd = pd;
   2135   auth_header = MHD_lookup_connection_value (connection,
   2136                                              MHD_HEADER_KIND,
   2137                                              MHD_HTTP_HEADER_AUTHORIZATION);
   2138   if (NULL != ps->webhook_token)
   2139   {
   2140     if (NULL == auth_header)
   2141     {
   2142       bad_auth = true;
   2143     }
   2144     else
   2145     {
   2146       struct GNUNET_HashCode wh_hash;
   2147       struct GNUNET_HashCode ah_hash;
   2148 
   2149       /* Compare the shared secret via hashes so that the comparison
   2150          time does not leak how many leading bytes of the token
   2151          matched (strcmp short-circuits and is a timing oracle). */
   2152       GNUNET_CRYPTO_hash (ps->webhook_token,
   2153                           strlen (ps->webhook_token),
   2154                           &wh_hash);
   2155       GNUNET_CRYPTO_hash (auth_header,
   2156                           strlen (auth_header),
   2157                           &ah_hash);
   2158       bad_auth = (0 != GNUNET_memcmp_priv (&wh_hash,
   2159                                            &ah_hash));
   2160     }
   2161   }
   2162   if (bad_auth)
   2163   {
   2164     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
   2165                 "Invalid authorization header `%s' received for Persona webhook\n",
   2166                 auth_header);
   2167     wh->resp = TALER_MHD_MAKE_JSON_PACK (
   2168       TALER_JSON_pack_ec (
   2169         TALER_EC_EXCHANGE_KYC_WEBHOOK_UNAUTHORIZED),
   2170       GNUNET_JSON_pack_string ("detail",
   2171                                "unexpected 'Authorization' header"));
   2172     wh->response_code = MHD_HTTP_UNAUTHORIZED;
   2173     wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply,
   2174                                          wh);
   2175     return wh;
   2176   }
   2177 
   2178   wh->template_id
   2179     = json_string_value (
   2180         json_object_get (
   2181           json_object_get (
   2182             json_object_get (
   2183               json_object_get (
   2184                 json_object_get (
   2185                   json_object_get (
   2186                     json_object_get (
   2187                       json_object_get (
   2188                         body,
   2189                         "data"),
   2190                       "attributes"),
   2191                     "payload"),
   2192                   "data"),
   2193                 "relationships"),
   2194               "inquiry-template"),
   2195             "data"),
   2196           "id"));
   2197   if (NULL == wh->template_id)
   2198   {
   2199     GNUNET_break_op (0);
   2200 #if DEBUG
   2201     json_dumpf (body,
   2202                 stderr,
   2203                 JSON_INDENT (2));
   2204 #endif
   2205     wh->resp = TALER_MHD_MAKE_JSON_PACK (
   2206       TALER_JSON_pack_ec (
   2207         TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY),
   2208       GNUNET_JSON_pack_string ("detail",
   2209                                "data-attributes-payload-data-id"),
   2210       GNUNET_JSON_pack_object_incref ("webhook_body",
   2211                                       (json_t *) body));
   2212     wh->response_code = MHD_HTTP_BAD_REQUEST;
   2213     wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply,
   2214                                          wh);
   2215     return wh;
   2216   }
   2217   TALER_KYCLOGIC_kyc_get_details ("persona",
   2218                                   &locate_details_cb,
   2219                                   wh);
   2220   if (NULL == wh->pd)
   2221   {
   2222     GNUNET_break_op (0);
   2223 #if DEBUG
   2224     json_dumpf (body,
   2225                 stderr,
   2226                 JSON_INDENT (2));
   2227 #endif
   2228     wh->resp = TALER_MHD_MAKE_JSON_PACK (
   2229       TALER_JSON_pack_ec (
   2230         TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN),
   2231       GNUNET_JSON_pack_string ("detail",
   2232                                wh->template_id),
   2233       GNUNET_JSON_pack_object_incref ("webhook_body",
   2234                                       (json_t *) body));
   2235     wh->response_code = MHD_HTTP_BAD_REQUEST;
   2236     wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply,
   2237                                          wh);
   2238     return wh;
   2239   }
   2240 
   2241   persona_inquiry_id
   2242     = json_string_value (
   2243         json_object_get (
   2244           json_object_get (
   2245             json_object_get (
   2246               json_object_get (
   2247                 json_object_get (
   2248                   body,
   2249                   "data"),
   2250                 "attributes"),
   2251               "payload"),
   2252             "data"),
   2253           "id"));
   2254   if (NULL == persona_inquiry_id)
   2255   {
   2256     GNUNET_break_op (0);
   2257 #if DEBUG
   2258     json_dumpf (body,
   2259                 stderr,
   2260                 JSON_INDENT (2));
   2261 #endif
   2262     wh->resp = TALER_MHD_MAKE_JSON_PACK (
   2263       TALER_JSON_pack_ec (
   2264         TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY),
   2265       GNUNET_JSON_pack_string ("detail",
   2266                                "data-attributes-payload-data-id"),
   2267       GNUNET_JSON_pack_object_incref ("webhook_body",
   2268                                       (json_t *) body));
   2269     wh->response_code = MHD_HTTP_BAD_REQUEST;
   2270     wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply,
   2271                                          wh);
   2272     return wh;
   2273   }
   2274   qs = plc (plc_cls,
   2275             wh->pd->provider_name,
   2276             persona_inquiry_id,
   2277             &wh->h_payto,
   2278             &wh->is_wallet,
   2279             &wh->process_row);
   2280   if (qs < 0)
   2281   {
   2282     wh->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED,
   2283                                      "provider-legitimization-lookup");
   2284     wh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
   2285     wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply,
   2286                                          wh);
   2287     return wh;
   2288   }
   2289   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
   2290   {
   2291     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
   2292                 "Received Persona kyc-webhook for unknown verification ID `%s'\n",
   2293                 persona_inquiry_id);
   2294     wh->resp = TALER_MHD_make_error (
   2295       TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN,
   2296       persona_inquiry_id);
   2297     wh->response_code = MHD_HTTP_NOT_FOUND;
   2298     wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply,
   2299                                          wh);
   2300     return wh;
   2301   }
   2302   wh->inquiry_id = GNUNET_strdup (persona_inquiry_id);
   2303 
   2304   eh = curl_easy_init ();
   2305   if (NULL == eh)
   2306   {
   2307     GNUNET_break (0);
   2308     wh->resp = TALER_MHD_make_error (
   2309       TALER_EC_GENERIC_ALLOCATION_FAILURE,
   2310       NULL);
   2311     wh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
   2312     wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply,
   2313                                          wh);
   2314     return wh;
   2315   }
   2316 
   2317   {
   2318     char *inquiry_id_encoded;
   2319 
   2320     inquiry_id_encoded = TALER_urlencode (persona_inquiry_id);
   2321     GNUNET_asprintf (&wh->url,
   2322                      "https://withpersona.com/api/v1/inquiries/%s",
   2323                      inquiry_id_encoded);
   2324     GNUNET_free (inquiry_id_encoded);
   2325   }
   2326   GNUNET_break (CURLE_OK ==
   2327                 curl_easy_setopt (eh,
   2328                                   CURLOPT_VERBOSE,
   2329                                   0));
   2330   GNUNET_assert (CURLE_OK ==
   2331                  curl_easy_setopt (eh,
   2332                                    CURLOPT_MAXREDIRS,
   2333                                    1L));
   2334   GNUNET_break (CURLE_OK ==
   2335                 curl_easy_setopt (eh,
   2336                                   CURLOPT_URL,
   2337                                   wh->url));
   2338   wh->job = GNUNET_CURL_job_add2 (ps->curl_ctx,
   2339                                   eh,
   2340                                   wh->pd->slist,
   2341                                   &handle_webhook_finished,
   2342                                   wh);
   2343   return wh;
   2344 }
   2345 
   2346 
   2347 /**
   2348  * Initialize persona logic plugin
   2349  *
   2350  * @param cls a configuration instance
   2351  * @return NULL on error, otherwise a `struct TALER_KYCLOGIC_Plugin`
   2352  */
   2353 void *
   2354 libtaler_plugin_kyclogic_persona_init (void *cls);
   2355 
   2356 /* declaration to avoid compiler warning */
   2357 void *
   2358 libtaler_plugin_kyclogic_persona_init (void *cls)
   2359 {
   2360   const struct GNUNET_CONFIGURATION_Handle *cfg = cls;
   2361   struct TALER_KYCLOGIC_Plugin *plugin;
   2362   struct PluginState *ps;
   2363 
   2364   ps = GNUNET_new (struct PluginState);
   2365   ps->cfg = cfg;
   2366   if (GNUNET_OK !=
   2367       GNUNET_CONFIGURATION_get_value_string (cfg,
   2368                                              "exchange",
   2369                                              "BASE_URL",
   2370                                              &ps->exchange_base_url))
   2371   {
   2372     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
   2373                                "exchange",
   2374                                "BASE_URL");
   2375     GNUNET_free (ps);
   2376     return NULL;
   2377   }
   2378   if (GNUNET_OK !=
   2379       GNUNET_CONFIGURATION_get_value_string (ps->cfg,
   2380                                              "kyclogic-persona",
   2381                                              "WEBHOOK_AUTH_TOKEN",
   2382                                              &ps->webhook_token))
   2383   {
   2384     /* optional */
   2385     ps->webhook_token = NULL;
   2386   }
   2387 
   2388   ps->curl_ctx
   2389     = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
   2390                         &ps->curl_rc);
   2391   if (NULL == ps->curl_ctx)
   2392   {
   2393     GNUNET_break (0);
   2394     GNUNET_free (ps->exchange_base_url);
   2395     GNUNET_free (ps);
   2396     return NULL;
   2397   }
   2398   ps->curl_rc = GNUNET_CURL_gnunet_rc_create (ps->curl_ctx);
   2399 
   2400   plugin = GNUNET_new (struct TALER_KYCLOGIC_Plugin);
   2401   plugin->cls = ps;
   2402   plugin->load_configuration
   2403     = &persona_load_configuration;
   2404   plugin->unload_configuration
   2405     = &persona_unload_configuration;
   2406   plugin->initiate
   2407     = &persona_initiate;
   2408   plugin->initiate_cancel
   2409     = &persona_initiate_cancel;
   2410   plugin->proof
   2411     = &persona_proof;
   2412   plugin->proof_cancel
   2413     = &persona_proof_cancel;
   2414   plugin->webhook
   2415     = &persona_webhook;
   2416   plugin->webhook_cancel
   2417     = &persona_webhook_cancel;
   2418   return plugin;
   2419 }
   2420 
   2421 
   2422 /**
   2423  * Unload authorization plugin
   2424  *
   2425  * @param cls a `struct TALER_KYCLOGIC_Plugin`
   2426  * @return NULL (always)
   2427  */
   2428 void *
   2429 libtaler_plugin_kyclogic_persona_done (void *cls);
   2430 
   2431 /* declaration to avoid compiler warning */
   2432 
   2433 void *
   2434 libtaler_plugin_kyclogic_persona_done (void *cls)
   2435 {
   2436   struct TALER_KYCLOGIC_Plugin *plugin = cls;
   2437   struct PluginState *ps = plugin->cls;
   2438 
   2439   if (NULL != ps->curl_ctx)
   2440   {
   2441     GNUNET_CURL_fini (ps->curl_ctx);
   2442     ps->curl_ctx = NULL;
   2443   }
   2444   if (NULL != ps->curl_rc)
   2445   {
   2446     GNUNET_CURL_gnunet_rc_destroy (ps->curl_rc);
   2447     ps->curl_rc = NULL;
   2448   }
   2449   GNUNET_free (ps->exchange_base_url);
   2450   GNUNET_free (ps->webhook_token);
   2451   GNUNET_free (ps);
   2452   GNUNET_free (plugin);
   2453   return NULL;
   2454 }
   2455 
   2456 
   2457 /* end of plugin_kyclogic_persona.c */