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


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