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


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