Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2023, 2024, 2026 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify it under the
6 : terms of the GNU 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 General Public License for more details.
12 :
13 : You should have received a copy of the GNU General Public License along with
14 : TALER; see the file COPYING. If not, see
15 : <http://www.gnu.org/licenses/>
16 : */
17 : /**
18 : * @file lib/exchange_api_post-aml-OFFICER_PUB-decision.c
19 : * @brief functions to add an AML decision by an AML officer
20 : * @author Christian Grothoff
21 : */
22 : #include "taler/taler_json_lib.h"
23 : #include <microhttpd.h>
24 : #include <gnunet/gnunet_curl_lib.h>
25 : #include "taler/exchange/post-aml-OFFICER_PUB-decision.h"
26 : #include "exchange_api_curl_defaults.h"
27 : #include "taler/taler_signatures.h"
28 : #include "taler/taler_curl_lib.h"
29 :
30 :
31 : /**
32 : * @brief A POST /aml/$OFFICER_PUB/decision Handle
33 : */
34 : struct TALER_EXCHANGE_PostAmlDecisionHandle
35 : {
36 :
37 : /**
38 : * The base URL of the exchange.
39 : */
40 : char *base_url;
41 :
42 : /**
43 : * The full URL for this request.
44 : */
45 : char *url;
46 :
47 : /**
48 : * Minor context that holds body and headers.
49 : */
50 : struct TALER_CURL_PostContext post_ctx;
51 :
52 : /**
53 : * Handle for the request.
54 : */
55 : struct GNUNET_CURL_Job *job;
56 :
57 : /**
58 : * Function to call with the result.
59 : */
60 : TALER_EXCHANGE_PostAmlDecisionCallback cb;
61 :
62 : /**
63 : * Closure for @e cb.
64 : */
65 : TALER_EXCHANGE_POST_AML_DECISION_RESULT_CLOSURE *cb_cls;
66 :
67 : /**
68 : * Reference to the execution context.
69 : */
70 : struct GNUNET_CURL_Context *ctx;
71 :
72 : /**
73 : * Public key of the AML officer.
74 : */
75 : struct TALER_AmlOfficerPublicKeyP officer_pub;
76 :
77 : /**
78 : * Private key of the AML officer.
79 : */
80 : struct TALER_AmlOfficerPrivateKeyP officer_priv;
81 :
82 : /**
83 : * Hash of the payto URI of the account the decision is about.
84 : */
85 : struct TALER_NormalizedPaytoHashP h_payto;
86 :
87 : /**
88 : * When was the decision made.
89 : */
90 : struct GNUNET_TIME_Timestamp decision_time;
91 :
92 : /**
93 : * Human-readable justification.
94 : */
95 : char *justification;
96 :
97 : /**
98 : * True to keep the investigation open.
99 : */
100 : bool keep_investigating;
101 :
102 : /**
103 : * Pre-built new_rules JSON object.
104 : */
105 : json_t *new_rules;
106 :
107 : /**
108 : * Optional: full payto URI string, may be NULL.
109 : */
110 : char *payto_uri_str;
111 :
112 : /**
113 : * Optional: space-separated list of measures to trigger immediately
114 : * (from options, may be NULL).
115 : */
116 : char *new_measures;
117 :
118 : /**
119 : * Optional: JSON object with account properties
120 : * (from options, may be NULL).
121 : */
122 : json_t *properties;
123 :
124 : /**
125 : * Optional: JSON array of events to trigger
126 : * (from options; may be NULL).
127 : */
128 : json_t *jevents;
129 :
130 : /**
131 : * Optional: JSON object with KYC attributes
132 : * (from options; may be NULL).
133 : */
134 : json_t *attributes;
135 :
136 : /**
137 : * Optional: expiration time for KYC attributes.
138 : * Only meaningful if @e attributes is non-NULL.
139 : */
140 : struct GNUNET_TIME_Timestamp attributes_expiration;
141 :
142 : };
143 :
144 :
145 : /**
146 : * Function called when we're done processing the
147 : * HTTP POST /aml/$OFFICER_PUB/decision request.
148 : *
149 : * @param cls the `struct TALER_EXCHANGE_PostAmlDecisionHandle *`
150 : * @param response_code HTTP response code, 0 on error
151 : * @param response response body, NULL if not in JSON
152 : */
153 : static void
154 3 : handle_post_aml_decision_finished (void *cls,
155 : long response_code,
156 : const void *response)
157 : {
158 3 : struct TALER_EXCHANGE_PostAmlDecisionHandle *padh = cls;
159 3 : const json_t *json = response;
160 3 : struct TALER_EXCHANGE_PostAmlDecisionResponse pr = {
161 3 : .hr.http_status = (unsigned int) response_code,
162 : .hr.reply = json
163 : };
164 :
165 3 : padh->job = NULL;
166 3 : switch (response_code)
167 : {
168 0 : case 0:
169 0 : pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
170 0 : pr.hr.hint = "server offline?";
171 0 : break;
172 2 : case MHD_HTTP_NO_CONTENT:
173 2 : break;
174 0 : case MHD_HTTP_FORBIDDEN:
175 0 : pr.hr.ec = TALER_JSON_get_error_code (json);
176 0 : pr.hr.hint = TALER_JSON_get_error_hint (json);
177 0 : break;
178 1 : case MHD_HTTP_CONFLICT:
179 1 : pr.hr.ec = TALER_JSON_get_error_code (json);
180 1 : pr.hr.hint = TALER_JSON_get_error_hint (json);
181 1 : break;
182 0 : default:
183 0 : GNUNET_break_op (0);
184 0 : pr.hr.ec = TALER_JSON_get_error_code (json);
185 0 : pr.hr.hint = TALER_JSON_get_error_hint (json);
186 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
187 : "Unexpected response code %u/%d for POST AML decision\n",
188 : (unsigned int) response_code,
189 : (int) pr.hr.ec);
190 0 : break;
191 : }
192 3 : if (NULL != padh->cb)
193 : {
194 3 : padh->cb (padh->cb_cls,
195 : &pr);
196 3 : padh->cb = NULL;
197 : }
198 3 : TALER_EXCHANGE_post_aml_decision_cancel (padh);
199 3 : }
200 :
201 :
202 : /**
203 : * Build the new_rules JSON object from rules and measures arrays.
204 : *
205 : * @param successor_measure optional successor measure name
206 : * @param expiration_time when the new rules expire
207 : * @param num_rules length of @a rules
208 : * @param rules the rules array
209 : * @param num_measures length of @a measures
210 : * @param measures the measures array
211 : * @return new JSON object (caller owns reference), NULL on error
212 : */
213 : static json_t *
214 3 : build_new_rules (
215 : const char *successor_measure,
216 : struct GNUNET_TIME_Timestamp expiration_time,
217 : unsigned int num_rules,
218 : const struct TALER_EXCHANGE_AccountRule rules[static num_rules],
219 : unsigned int num_measures,
220 : const struct TALER_EXCHANGE_MeasureInformation measures[static num_measures])
221 3 : {
222 : json_t *jrules;
223 : json_t *jmeasures;
224 :
225 3 : jrules = json_array ();
226 3 : GNUNET_assert (NULL != jrules);
227 6 : for (unsigned int i = 0; i < num_rules; i++)
228 : {
229 3 : const struct TALER_EXCHANGE_AccountRule *al = &rules[i];
230 : json_t *ameasures;
231 : json_t *rule;
232 :
233 3 : ameasures = json_array ();
234 3 : GNUNET_assert (NULL != ameasures);
235 4 : for (unsigned int j = 0; j < al->num_measures; j++)
236 1 : GNUNET_assert (0 ==
237 : json_array_append_new (ameasures,
238 : json_string (al->measures[j])));
239 3 : rule = GNUNET_JSON_PACK (
240 : TALER_JSON_pack_kycte ("operation_type",
241 : al->operation_type),
242 : TALER_JSON_pack_amount ("threshold",
243 : &al->threshold),
244 : GNUNET_JSON_pack_time_rel ("timeframe",
245 : al->timeframe),
246 : GNUNET_JSON_pack_array_steal ("measures",
247 : ameasures),
248 : GNUNET_JSON_pack_allow_null (
249 : GNUNET_JSON_pack_string ("rule_name",
250 : al->rule_name)),
251 : GNUNET_JSON_pack_bool ("exposed",
252 : al->exposed),
253 : GNUNET_JSON_pack_bool ("is_and_combinator",
254 : al->is_and_combinator),
255 : GNUNET_JSON_pack_uint64 ("display_priority",
256 : al->display_priority)
257 : );
258 3 : GNUNET_break (0 ==
259 : json_array_append_new (jrules,
260 : rule));
261 : }
262 :
263 3 : jmeasures = json_object ();
264 3 : GNUNET_assert (NULL != jmeasures);
265 4 : for (unsigned int i = 0; i < num_measures; i++)
266 : {
267 1 : const struct TALER_EXCHANGE_MeasureInformation *mi = &measures[i];
268 : json_t *measure;
269 :
270 1 : measure = GNUNET_JSON_PACK (
271 : GNUNET_JSON_pack_string ("check_name",
272 : mi->check_name),
273 : GNUNET_JSON_pack_allow_null (
274 : GNUNET_JSON_pack_string ("prog_name",
275 : mi->prog_name)),
276 : /* We pack "NONE" for unknown, NULL would also be OK in that case. */
277 : TALER_JSON_pack_kycte ("operation_type",
278 : mi->operation_type),
279 : GNUNET_JSON_pack_allow_null (
280 : GNUNET_JSON_pack_object_incref ("context",
281 : (json_t *) mi->context))
282 : );
283 1 : GNUNET_break (0 ==
284 : json_object_set_new (jmeasures,
285 : mi->measure_name,
286 : measure));
287 : }
288 :
289 3 : return GNUNET_JSON_PACK (
290 : GNUNET_JSON_pack_timestamp ("expiration_time",
291 : expiration_time),
292 : GNUNET_JSON_pack_allow_null (
293 : GNUNET_JSON_pack_string ("successor_measure",
294 : successor_measure)),
295 : GNUNET_JSON_pack_array_steal ("rules",
296 : jrules),
297 : GNUNET_JSON_pack_object_steal ("custom_measures",
298 : jmeasures)
299 : );
300 : }
301 :
302 :
303 : struct TALER_EXCHANGE_PostAmlDecisionHandle *
304 3 : TALER_EXCHANGE_post_aml_decision_create (
305 : struct GNUNET_CURL_Context *ctx,
306 : const char *url,
307 : const struct TALER_NormalizedPaytoHashP *h_payto,
308 : struct GNUNET_TIME_Timestamp decision_time,
309 : const char *successor_measure,
310 : struct GNUNET_TIME_Timestamp expiration_time,
311 : unsigned int num_rules,
312 : const struct TALER_EXCHANGE_AccountRule rules[static num_rules],
313 : unsigned int num_measures,
314 : const struct TALER_EXCHANGE_MeasureInformation measures[static num_measures],
315 : bool keep_investigating,
316 : const char *justification,
317 : const struct TALER_AmlOfficerPrivateKeyP *officer_priv)
318 3 : {
319 : struct TALER_EXCHANGE_PostAmlDecisionHandle *padh;
320 : json_t *new_rules;
321 :
322 3 : new_rules = build_new_rules (successor_measure,
323 : expiration_time,
324 : num_rules,
325 : rules,
326 : num_measures,
327 : measures);
328 3 : if (NULL == new_rules)
329 : {
330 0 : GNUNET_break (0);
331 0 : return NULL;
332 : }
333 :
334 3 : padh = GNUNET_new (struct TALER_EXCHANGE_PostAmlDecisionHandle);
335 3 : padh->ctx = ctx;
336 3 : padh->base_url = GNUNET_strdup (url);
337 3 : padh->h_payto = *h_payto;
338 3 : padh->decision_time = decision_time;
339 3 : padh->justification = GNUNET_strdup (justification);
340 3 : padh->keep_investigating = keep_investigating;
341 3 : padh->new_rules = new_rules;
342 3 : padh->officer_priv = *officer_priv;
343 3 : GNUNET_CRYPTO_eddsa_key_get_public (&officer_priv->eddsa_priv,
344 : &padh->officer_pub.eddsa_pub);
345 3 : return padh;
346 : }
347 :
348 :
349 : enum GNUNET_GenericReturnValue
350 4 : TALER_EXCHANGE_post_aml_decision_set_options_ (
351 : struct TALER_EXCHANGE_PostAmlDecisionHandle *padh,
352 : unsigned int num_options,
353 : const struct TALER_EXCHANGE_PostAmlDecisionOptionValue options[])
354 : {
355 8 : for (unsigned int i = 0; i < num_options; i++)
356 : {
357 8 : const struct TALER_EXCHANGE_PostAmlDecisionOptionValue *opt = &options[i];
358 :
359 8 : switch (opt->option)
360 : {
361 4 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_END:
362 4 : return GNUNET_OK;
363 0 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_PAYTO_URI:
364 0 : GNUNET_free (padh->payto_uri_str);
365 0 : padh->payto_uri_str =
366 0 : (NULL != opt->details.payto_uri.full_payto)
367 0 : ? GNUNET_strdup (opt->details.payto_uri.full_payto)
368 0 : : NULL;
369 0 : break;
370 1 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_NEW_MEASURES:
371 1 : GNUNET_free (padh->new_measures);
372 1 : padh->new_measures =
373 1 : (NULL != opt->details.new_measures)
374 1 : ? GNUNET_strdup (opt->details.new_measures)
375 1 : : NULL;
376 1 : break;
377 3 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_PROPERTIES:
378 3 : if (NULL != padh->properties)
379 0 : json_decref (padh->properties);
380 3 : padh->properties =
381 3 : (NULL != opt->details.properties)
382 3 : ? json_incref ((json_t *) opt->details.properties)
383 3 : : NULL;
384 3 : break;
385 0 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_EVENTS:
386 : {
387 0 : if (NULL != padh->jevents)
388 0 : json_decref (padh->jevents);
389 0 : if (0 == opt->details.events.num_events)
390 : {
391 0 : padh->jevents = NULL;
392 : }
393 : else
394 : {
395 0 : padh->jevents = json_array ();
396 0 : GNUNET_assert (NULL != padh->jevents);
397 0 : for (unsigned int j = 0; j < opt->details.events.num_events; j++)
398 0 : GNUNET_assert (0 ==
399 : json_array_append_new (
400 : padh->jevents,
401 : json_string (opt->details.events.events[j])));
402 : }
403 : }
404 0 : break;
405 0 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_ATTRIBUTES:
406 0 : if (NULL != padh->attributes)
407 0 : json_decref (padh->attributes);
408 0 : padh->attributes =
409 0 : (NULL != opt->details.attributes)
410 0 : ? json_incref ((json_t *) opt->details.attributes)
411 0 : : NULL;
412 0 : break;
413 0 : case TALER_EXCHANGE_POST_AML_DECISION_OPTION_ATTRIBUTES_EXPIRATION:
414 0 : padh->attributes_expiration = opt->details.attributes_expiration;
415 0 : break;
416 0 : default:
417 0 : GNUNET_break (0);
418 0 : return GNUNET_SYSERR;
419 : }
420 : }
421 0 : return GNUNET_OK;
422 : }
423 :
424 :
425 : enum TALER_ErrorCode
426 3 : TALER_EXCHANGE_post_aml_decision_start (
427 : struct TALER_EXCHANGE_PostAmlDecisionHandle *padh,
428 : TALER_EXCHANGE_PostAmlDecisionCallback cb,
429 : TALER_EXCHANGE_POST_AML_DECISION_RESULT_CLOSURE *cb_cls)
430 : {
431 : CURL *eh;
432 : struct TALER_AmlOfficerSignatureP officer_sig;
433 : json_t *body;
434 : char *path;
435 : char opus[sizeof (padh->officer_pub) * 2];
436 : char *end;
437 3 : struct TALER_FullPayto payto_uri_val = {
438 3 : .full_payto = padh->payto_uri_str
439 : };
440 :
441 3 : padh->cb = cb;
442 3 : padh->cb_cls = cb_cls;
443 3 : TALER_officer_aml_decision_sign (padh->justification,
444 : padh->decision_time,
445 3 : &padh->h_payto,
446 3 : padh->new_rules,
447 3 : padh->properties,
448 3 : padh->new_measures,
449 3 : padh->keep_investigating,
450 3 : &padh->officer_priv,
451 : &officer_sig);
452 :
453 3 : end = GNUNET_STRINGS_data_to_string (
454 3 : &padh->officer_pub,
455 : sizeof (padh->officer_pub),
456 : opus,
457 : sizeof (opus));
458 3 : *end = '\0';
459 3 : GNUNET_asprintf (&path,
460 : "aml/%s/decision",
461 : opus);
462 3 : padh->url = TALER_url_join (padh->base_url,
463 : path,
464 : NULL);
465 3 : GNUNET_free (path);
466 3 : if (NULL == padh->url)
467 : {
468 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
469 : "Could not construct request URL.\n");
470 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
471 : }
472 :
473 3 : body = GNUNET_JSON_PACK (
474 : GNUNET_JSON_pack_string ("justification",
475 : padh->justification),
476 : GNUNET_JSON_pack_data_auto ("h_payto",
477 : &padh->h_payto),
478 : GNUNET_JSON_pack_allow_null (
479 : TALER_JSON_pack_full_payto ("payto_uri",
480 : payto_uri_val)),
481 : GNUNET_JSON_pack_object_incref ("new_rules",
482 : padh->new_rules),
483 : GNUNET_JSON_pack_allow_null (
484 : GNUNET_JSON_pack_object_incref ("properties",
485 : padh->properties)),
486 : GNUNET_JSON_pack_allow_null (
487 : GNUNET_JSON_pack_string ("new_measures",
488 : padh->new_measures)),
489 : GNUNET_JSON_pack_bool ("keep_investigating",
490 : padh->keep_investigating),
491 : GNUNET_JSON_pack_data_auto ("officer_sig",
492 : &officer_sig),
493 : GNUNET_JSON_pack_timestamp ("decision_time",
494 : padh->decision_time),
495 : GNUNET_JSON_pack_allow_null (
496 : GNUNET_JSON_pack_array_incref ("events",
497 : padh->jevents)),
498 : GNUNET_JSON_pack_allow_null (
499 : GNUNET_JSON_pack_object_incref ("attributes",
500 : padh->attributes))
501 : );
502 3 : if (NULL != padh->attributes)
503 : {
504 0 : GNUNET_assert (
505 : 0 ==
506 : json_object_set_new (
507 : body,
508 : "attributes_expiration",
509 : GNUNET_JSON_from_timestamp (padh->attributes_expiration)));
510 : }
511 :
512 3 : eh = TALER_EXCHANGE_curl_easy_get_ (padh->url);
513 6 : if ( (NULL == eh) ||
514 : (GNUNET_OK !=
515 3 : TALER_curl_easy_post (&padh->post_ctx,
516 : eh,
517 : body)) )
518 : {
519 0 : GNUNET_break (0);
520 0 : if (NULL != eh)
521 0 : curl_easy_cleanup (eh);
522 0 : json_decref (body);
523 0 : GNUNET_free (padh->url);
524 0 : padh->url = NULL;
525 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
526 : }
527 3 : json_decref (body);
528 3 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
529 : "Requesting URL '%s'\n",
530 : padh->url);
531 6 : padh->job = GNUNET_CURL_job_add2 (padh->ctx,
532 : eh,
533 3 : padh->post_ctx.headers,
534 : &handle_post_aml_decision_finished,
535 : padh);
536 3 : if (NULL == padh->job)
537 : {
538 0 : TALER_curl_easy_post_finished (&padh->post_ctx);
539 0 : GNUNET_free (padh->url);
540 0 : padh->url = NULL;
541 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
542 : }
543 3 : return TALER_EC_NONE;
544 : }
545 :
546 :
547 : void
548 3 : TALER_EXCHANGE_post_aml_decision_cancel (
549 : struct TALER_EXCHANGE_PostAmlDecisionHandle *padh)
550 : {
551 3 : if (NULL != padh->job)
552 : {
553 0 : GNUNET_CURL_job_cancel (padh->job);
554 0 : padh->job = NULL;
555 : }
556 3 : TALER_curl_easy_post_finished (&padh->post_ctx);
557 3 : json_decref (padh->new_rules);
558 3 : if (NULL != padh->properties)
559 3 : json_decref (padh->properties);
560 3 : if (NULL != padh->jevents)
561 0 : json_decref (padh->jevents);
562 3 : if (NULL != padh->attributes)
563 0 : json_decref (padh->attributes);
564 3 : GNUNET_free (padh->url);
565 3 : GNUNET_free (padh->base_url);
566 3 : GNUNET_free (padh->justification);
567 3 : GNUNET_free (padh->payto_uri_str);
568 3 : GNUNET_free (padh->new_measures);
569 3 : GNUNET_free (padh);
570 3 : }
571 :
572 :
573 : /* end of exchange_api_post-aml-OFFICER_PUB-decision.c */
|