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