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 (68380B)


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