Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 2025 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify
6 : it under the terms of the GNU Affero General Public License as
7 : published by the Free Software Foundation; either version 3,
8 : or (at your option) any later version.
9 :
10 : TALER is distributed in the hope that it will be useful, but
11 : WITHOUT ANY WARRANTY; without even the implied warranty of
12 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : GNU General Public License for more details.
14 :
15 : You should have received a copy of the GNU General Public
16 : License along with TALER; see the file COPYING. If not,
17 : see <http://www.gnu.org/licenses/>
18 : */
19 :
20 : /**
21 : * @file taler-merchant-httpd_post-challenge-ID.c
22 : * @brief endpoint to trigger sending MFA challenge
23 : * @author Christian Grothoff
24 : */
25 : #include "platform.h"
26 : #include "taler-merchant-httpd.h"
27 : #include "taler-merchant-httpd_mfa.h"
28 : #include "taler-merchant-httpd_post-challenge-ID.h"
29 :
30 :
31 : /**
32 : * How many attempts do we allow per solution at most? Note that
33 : * this is just for the API, the value must also match the
34 : * database logic in create_mfa_challenge.
35 : */
36 : #define MAX_SOLUTIONS 3
37 :
38 :
39 : /**
40 : * How long is an OTP code valid?
41 : */
42 : #define OTP_TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 30)
43 :
44 :
45 : /**
46 : * Internal state for MFA processing.
47 : */
48 : struct MfaState
49 : {
50 :
51 : /**
52 : * Kept in a DLL.
53 : */
54 : struct MfaState *next;
55 :
56 : /**
57 : * Kept in a DLL.
58 : */
59 : struct MfaState *prev;
60 :
61 : /**
62 : * HTTP request we are handling.
63 : */
64 : struct TMH_HandlerContext *hc;
65 :
66 : /**
67 : * Challenge code.
68 : */
69 : char *code;
70 :
71 : /**
72 : * When does @e code expire?
73 : */
74 : struct GNUNET_TIME_Absolute expiration_date;
75 :
76 : /**
77 : * When may we transmit a new code?
78 : */
79 : struct GNUNET_TIME_Absolute retransmission_date;
80 :
81 : /**
82 : * Handle to the helper process.
83 : */
84 : struct GNUNET_OS_Process *child;
85 :
86 : /**
87 : * Handle to wait for @e child
88 : */
89 : struct GNUNET_ChildWaitHandle *cwh;
90 :
91 : /**
92 : * Address where to send the challenge.
93 : */
94 : char *required_address;
95 :
96 : /**
97 : * Message to send.
98 : */
99 : char *msg;
100 :
101 : /**
102 : * Offset of transmission in msg.
103 : */
104 : size_t msg_off;
105 :
106 : /**
107 : * ID of our challenge.
108 : */
109 : uint64_t challenge_id;
110 :
111 : /**
112 : * Salted hash over the request body.
113 : */
114 : struct TALER_MERCHANT_MFA_BodyHash h_body;
115 :
116 : /**
117 : * Channel to use for the challenge.
118 : */
119 : enum TALER_MERCHANT_MFA_Channel channel;
120 :
121 : enum
122 : {
123 : MFA_PHASE_PARSE = 0,
124 : MFA_PHASE_LOOKUP,
125 : MFA_PHASE_SENDING,
126 : MFA_PHASE_SUSPENDING,
127 : MFA_PHASE_SENT,
128 : MFA_PHASE_RETURN_YES,
129 : MFA_PHASE_RETURN_NO,
130 :
131 : } phase;
132 :
133 :
134 : /**
135 : * #GNUNET_NO if the @e connection was not suspended,
136 : * #GNUNET_YES if the @e connection was suspended,
137 : * #GNUNET_SYSERR if @e connection was resumed to as
138 : * part of #THM_mfa_done during shutdown.
139 : */
140 : enum GNUNET_GenericReturnValue suspended;
141 :
142 : /**
143 : * Type of critical operation being authorized.
144 : */
145 : enum TALER_MERCHANT_MFA_CriticalOperation op;
146 :
147 : /**
148 : * Set to true if sending worked.
149 : */
150 : bool send_ok;
151 : };
152 :
153 :
154 : /**
155 : * Kept in a DLL.
156 : */
157 : static struct MfaState *mfa_head;
158 :
159 : /**
160 : * Kept in a DLL.
161 : */
162 : static struct MfaState *mfa_tail;
163 :
164 :
165 : /**
166 : * Clean up @a mfa process.
167 : *
168 : * @param[in] cls the `struct MfaState` to clean up
169 : */
170 : static void
171 0 : mfa_context_cleanup (void *cls)
172 : {
173 0 : struct MfaState *mfa = cls;
174 :
175 0 : GNUNET_CONTAINER_DLL_remove (mfa_head,
176 : mfa_tail,
177 : mfa);
178 0 : if (NULL != mfa->cwh)
179 : {
180 0 : GNUNET_wait_child_cancel (mfa->cwh);
181 0 : mfa->cwh = NULL;
182 : }
183 0 : if (NULL != mfa->child)
184 : {
185 0 : (void) GNUNET_OS_process_kill (mfa->child,
186 : SIGKILL);
187 0 : GNUNET_break (GNUNET_OK ==
188 : GNUNET_OS_process_wait (mfa->child));
189 0 : mfa->child = NULL;
190 : }
191 0 : GNUNET_free (mfa->required_address);
192 0 : GNUNET_free (mfa->msg);
193 0 : GNUNET_free (mfa->code);
194 0 : GNUNET_free (mfa);
195 0 : }
196 :
197 :
198 : void
199 15 : TMH_challenge_done ()
200 : {
201 15 : for (struct MfaState *mfa = mfa_head;
202 15 : NULL != mfa;
203 0 : mfa = mfa->next)
204 : {
205 0 : if (GNUNET_YES == mfa->suspended)
206 : {
207 0 : mfa->suspended = GNUNET_SYSERR;
208 0 : MHD_resume_connection (mfa->hc->connection);
209 : }
210 : }
211 15 : }
212 :
213 :
214 : /**
215 : * Send the given @a response for the @a mfa request.
216 : *
217 : * @param[in,out] mfa process to generate an error response for
218 : * @param response_code response code to use
219 : * @param[in] response response data to send back
220 : */
221 : static void
222 0 : respond_to_challenge_with_response (struct MfaState *mfa,
223 : unsigned int response_code,
224 : struct MHD_Response *response)
225 : {
226 : MHD_RESULT res;
227 :
228 0 : res = MHD_queue_response (mfa->hc->connection,
229 : response_code,
230 : response);
231 0 : MHD_destroy_response (response);
232 0 : mfa->phase = (MHD_NO == res)
233 : ? MFA_PHASE_RETURN_NO
234 0 : : MFA_PHASE_RETURN_YES;
235 0 : }
236 :
237 :
238 : /**
239 : * Generate an error for @a mfa.
240 : *
241 : * @param[in,out] mfa process to generate an error response for
242 : * @param http_status HTTP status of the response
243 : * @param ec Taler error code to return
244 : * @param hint hint to return, can be NULL
245 : */
246 : static void
247 0 : respond_with_error (struct MfaState *mfa,
248 : unsigned int http_status,
249 : enum TALER_ErrorCode ec,
250 : const char *hint)
251 : {
252 0 : respond_to_challenge_with_response (
253 : mfa,
254 : http_status,
255 : TALER_MHD_make_error (ec,
256 : hint));
257 0 : }
258 :
259 :
260 : /**
261 : * Challenge code transmission complete. Continue based on the result.
262 : *
263 : * @param[in,out] mfa process to send the challenge for
264 : */
265 : static void
266 0 : phase_sent (struct MfaState *mfa)
267 : {
268 : enum GNUNET_DB_QueryStatus qs;
269 :
270 0 : if (! mfa->send_ok)
271 : {
272 0 : respond_with_error (mfa,
273 : MHD_HTTP_INTERNAL_SERVER_ERROR,
274 : TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
275 : "process exited with error");
276 0 : return;
277 : }
278 0 : qs = TMH_db->update_mfa_challenge (TMH_db->cls,
279 : mfa->challenge_id,
280 0 : mfa->code,
281 : MAX_SOLUTIONS,
282 : mfa->expiration_date,
283 : mfa->retransmission_date);
284 0 : switch (qs)
285 : {
286 0 : case GNUNET_DB_STATUS_HARD_ERROR:
287 0 : GNUNET_break (0);
288 0 : respond_with_error (mfa,
289 : MHD_HTTP_INTERNAL_SERVER_ERROR,
290 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
291 : "update_mfa_challenge");
292 0 : return;
293 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
294 0 : GNUNET_break (0);
295 0 : respond_with_error (mfa,
296 : MHD_HTTP_INTERNAL_SERVER_ERROR,
297 : TALER_EC_GENERIC_DB_SOFT_FAILURE,
298 : "update_mfa_challenge");
299 0 : return;
300 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
301 0 : GNUNET_break (0);
302 0 : respond_with_error (mfa,
303 : MHD_HTTP_INTERNAL_SERVER_ERROR,
304 : TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
305 : "no results on INSERT, but success?");
306 0 : return;
307 0 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
308 0 : break;
309 : }
310 : {
311 : struct MHD_Response *response;
312 :
313 : response =
314 0 : TALER_MHD_make_json_steal (
315 0 : GNUNET_JSON_PACK (
316 : GNUNET_JSON_pack_timestamp (
317 : "solve_expiration",
318 : GNUNET_TIME_absolute_to_timestamp (
319 : mfa->expiration_date)),
320 : GNUNET_JSON_pack_timestamp (
321 : "earliest_retransmission",
322 : GNUNET_TIME_absolute_to_timestamp (
323 : mfa->retransmission_date))));
324 0 : respond_to_challenge_with_response (
325 : mfa,
326 : MHD_HTTP_OK,
327 : response);
328 : }
329 : }
330 :
331 :
332 : /**
333 : * Function called when our SMS helper has terminated.
334 : *
335 : * @param cls our `struct ANASTASIS_AUHTORIZATION_State`
336 : * @param type type of the process
337 : * @param exit_code status code of the process
338 : */
339 : static void
340 0 : transmission_done_cb (void *cls,
341 : enum GNUNET_OS_ProcessStatusType type,
342 : long unsigned int exit_code)
343 : {
344 0 : struct MfaState *mfa = cls;
345 :
346 0 : mfa->cwh = NULL;
347 0 : if (NULL != mfa->child)
348 : {
349 0 : GNUNET_OS_process_destroy (mfa->child);
350 0 : mfa->child = NULL;
351 : }
352 0 : mfa->send_ok = ( (GNUNET_OS_PROCESS_EXITED == type) &&
353 0 : (0 == exit_code) );
354 0 : if (! mfa->send_ok)
355 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
356 : "MFA helper failed with status %d/%u\n",
357 : (int) type,
358 : (unsigned int) exit_code);
359 0 : mfa->phase = MFA_PHASE_SENT;
360 0 : GNUNET_assert (GNUNET_YES == mfa->suspended);
361 0 : mfa->suspended = GNUNET_NO;
362 0 : MHD_resume_connection (mfa->hc->connection);
363 0 : TALER_MHD_daemon_trigger ();
364 0 : }
365 :
366 :
367 : /**
368 : * Setup challenge code for @a mfa and send it to the
369 : * @a required_address; on success.
370 : *
371 : * @param[in,out] mfa process to send the challenge for
372 : * @param required_address where to send the challenge
373 : */
374 : static void
375 0 : phase_send_challenge (struct MfaState *mfa)
376 : {
377 0 : const char *prog = NULL;
378 :
379 0 : switch (mfa->channel)
380 : {
381 0 : case TALER_MERCHANT_MFA_CHANNEL_NONE:
382 0 : GNUNET_assert (0);
383 : break;
384 0 : case TALER_MERCHANT_MFA_CHANNEL_SMS:
385 : mfa->expiration_date
386 0 : = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
387 : mfa->retransmission_date
388 0 : = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
389 0 : GNUNET_asprintf (&mfa->code,
390 : "%llu",
391 : (unsigned long long)
392 0 : GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE,
393 : 100000000));
394 0 : prog = TMH_helper_sms;
395 0 : break;
396 0 : case TALER_MERCHANT_MFA_CHANNEL_EMAIL:
397 : mfa->expiration_date
398 0 : = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
399 : mfa->retransmission_date
400 0 : = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
401 0 : GNUNET_asprintf (&mfa->code,
402 : "%llu",
403 : (unsigned long long)
404 0 : GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE,
405 : 100000000));
406 0 : prog = TMH_helper_email;
407 0 : break;
408 0 : case TALER_MERCHANT_MFA_CHANNEL_TOTP:
409 : mfa->expiration_date
410 0 : = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT);
411 : mfa->retransmission_date
412 0 : = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT);
413 0 : respond_with_error (mfa,
414 : MHD_HTTP_NOT_IMPLEMENTED,
415 : TALER_EC_GENERIC_FEATURE_NOT_IMPLEMENTED,
416 : "#10327");
417 0 : return;
418 : }
419 0 : if (NULL == prog)
420 : {
421 0 : respond_with_error (
422 : mfa,
423 : MHD_HTTP_INTERNAL_SERVER_ERROR,
424 : TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
425 : TALER_MERCHANT_MFA_channel_to_string (mfa->channel));
426 0 : return;
427 : }
428 : {
429 : /* Start child process and feed pipe */
430 : struct GNUNET_DISK_PipeHandle *p;
431 : struct GNUNET_DISK_FileHandle *pipe_stdin;
432 :
433 0 : p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW);
434 0 : if (NULL == p)
435 : {
436 0 : respond_with_error (mfa,
437 : MHD_HTTP_INTERNAL_SERVER_ERROR,
438 : TALER_EC_GENERIC_ALLOCATION_FAILURE,
439 : "pipe");
440 0 : return;
441 : }
442 0 : mfa->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR,
443 : p,
444 : NULL,
445 : NULL,
446 : prog,
447 : prog,
448 : mfa->required_address,
449 : NULL);
450 0 : if (NULL == mfa->child)
451 : {
452 0 : GNUNET_break (GNUNET_OK ==
453 : GNUNET_DISK_pipe_close (p));
454 0 : respond_with_error (mfa,
455 : MHD_HTTP_INTERNAL_SERVER_ERROR,
456 : TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
457 : "exec");
458 0 : return;
459 : }
460 :
461 0 : pipe_stdin = GNUNET_DISK_pipe_detach_end (p,
462 : GNUNET_DISK_PIPE_END_WRITE);
463 0 : GNUNET_assert (NULL != pipe_stdin);
464 0 : GNUNET_break (GNUNET_OK ==
465 : GNUNET_DISK_pipe_close (p));
466 0 : GNUNET_asprintf (&mfa->msg,
467 : "%s\nTaler-Merchant:\n%s",
468 : mfa->code,
469 : TALER_MERCHANT_MFA_co2s (mfa->op));
470 : {
471 0 : const char *off = mfa->msg;
472 0 : size_t left = strlen (off);
473 :
474 0 : while (0 != left)
475 : {
476 : ssize_t ret;
477 :
478 0 : ret = GNUNET_DISK_file_write (pipe_stdin,
479 : off,
480 : left);
481 0 : if (ret <= 0)
482 : {
483 0 : respond_with_error (mfa,
484 : MHD_HTTP_INTERNAL_SERVER_ERROR,
485 : TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
486 : "write");
487 0 : return;
488 : }
489 0 : mfa->msg_off += ret;
490 0 : off += ret;
491 0 : left -= ret;
492 : }
493 0 : GNUNET_DISK_file_close (pipe_stdin);
494 : }
495 : }
496 0 : mfa->phase = MFA_PHASE_SUSPENDING;
497 : }
498 :
499 :
500 : /**
501 : * Lookup challenge in DB.
502 : *
503 : * @param[in,out] mfa process to parse data for
504 : */
505 : static void
506 0 : phase_lookup (struct MfaState *mfa)
507 : {
508 : enum GNUNET_DB_QueryStatus qs;
509 : uint32_t retry_counter;
510 : struct GNUNET_TIME_Absolute confirmation_date;
511 : struct GNUNET_TIME_Absolute retransmission_date;
512 : struct TALER_MERCHANT_MFA_BodySalt salt;
513 :
514 0 : qs = TMH_db->lookup_mfa_challenge (TMH_db->cls,
515 : mfa->challenge_id,
516 0 : &mfa->h_body,
517 : &salt,
518 : &mfa->required_address,
519 : &mfa->op,
520 : &confirmation_date,
521 : &retransmission_date,
522 : &retry_counter,
523 : &mfa->channel);
524 0 : switch (qs)
525 : {
526 0 : case GNUNET_DB_STATUS_HARD_ERROR:
527 0 : GNUNET_break (0);
528 0 : respond_with_error (mfa,
529 : MHD_HTTP_INTERNAL_SERVER_ERROR,
530 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
531 : "lookup_mfa_challenge");
532 0 : return;
533 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
534 0 : GNUNET_break (0);
535 0 : respond_with_error (mfa,
536 : MHD_HTTP_INTERNAL_SERVER_ERROR,
537 : TALER_EC_GENERIC_DB_SOFT_FAILURE,
538 : "lookup_mfa_challenge");
539 0 : return;
540 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
541 0 : GNUNET_break (0);
542 0 : respond_with_error (mfa,
543 : MHD_HTTP_NOT_FOUND,
544 : TALER_EC_MERCHANT_TAN_CHALLENGE_UNKNOWN,
545 0 : mfa->hc->infix);
546 0 : return;
547 0 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
548 0 : break;
549 : }
550 0 : if (! GNUNET_TIME_absolute_is_future (confirmation_date))
551 : {
552 : /* was already solved */
553 0 : respond_with_error (mfa,
554 : MHD_HTTP_GONE,
555 : TALER_EC_MERCHANT_TAN_CHALLENGE_SOLVED,
556 : NULL);
557 0 : return;
558 : }
559 0 : if (GNUNET_TIME_absolute_is_future (retransmission_date))
560 : {
561 : /* too early to try again */
562 0 : respond_with_error (mfa,
563 : MHD_HTTP_TOO_MANY_REQUESTS,
564 : TALER_EC_MERCHANT_TAN_TOO_EARLY,
565 : GNUNET_TIME_absolute2s (retransmission_date));
566 0 : return;
567 : }
568 0 : mfa->phase++;
569 : }
570 :
571 :
572 : /**
573 : * Parse challenge request.
574 : *
575 : * @param[in,out] mfa process to parse data for
576 : */
577 : static void
578 0 : phase_parse (struct MfaState *mfa)
579 : {
580 0 : struct TMH_HandlerContext *hc = mfa->hc;
581 : enum GNUNET_GenericReturnValue ret;
582 :
583 0 : ret = TMH_mfa_parse_challenge_id (hc,
584 0 : hc->infix,
585 : &mfa->challenge_id,
586 : &mfa->h_body);
587 0 : if (GNUNET_OK != ret)
588 : {
589 0 : mfa->phase = (GNUNET_NO == ret)
590 : ? MFA_PHASE_RETURN_YES
591 0 : : MFA_PHASE_RETURN_NO;
592 0 : return;
593 : }
594 0 : mfa->phase++;
595 : }
596 :
597 :
598 : MHD_RESULT
599 0 : TMH_post_challenge_ID (const struct TMH_RequestHandler *rh,
600 : struct MHD_Connection *connection,
601 : struct TMH_HandlerContext *hc)
602 : {
603 0 : struct MfaState *mfa = hc->ctx;
604 :
605 0 : if (NULL == mfa)
606 : {
607 0 : mfa = GNUNET_new (struct MfaState);
608 0 : mfa->hc = hc;
609 0 : hc->ctx = mfa;
610 0 : hc->cc = &mfa_context_cleanup;
611 0 : GNUNET_CONTAINER_DLL_insert (mfa_head,
612 : mfa_tail,
613 : mfa);
614 : }
615 :
616 : while (1)
617 : {
618 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
619 : "Processing /challenge in phase %d\n",
620 : (int) mfa->phase);
621 0 : switch (mfa->phase)
622 : {
623 0 : case MFA_PHASE_PARSE:
624 0 : phase_parse (mfa);
625 0 : break;
626 0 : case MFA_PHASE_LOOKUP:
627 0 : phase_lookup (mfa);
628 0 : break;
629 0 : case MFA_PHASE_SENDING:
630 0 : phase_send_challenge (mfa);
631 0 : break;
632 0 : case MFA_PHASE_SUSPENDING:
633 0 : mfa->cwh = GNUNET_wait_child (mfa->child,
634 : &transmission_done_cb,
635 : mfa);
636 0 : if (NULL == mfa->cwh)
637 : {
638 0 : respond_with_error (mfa,
639 : MHD_HTTP_INTERNAL_SERVER_ERROR,
640 : TALER_EC_GENERIC_ALLOCATION_FAILURE,
641 : "GNUNET_wait_child");
642 0 : continue;
643 : }
644 0 : mfa->suspended = GNUNET_YES;
645 0 : MHD_suspend_connection (hc->connection);
646 0 : return MHD_YES;
647 0 : case MFA_PHASE_SENT:
648 0 : phase_sent (mfa);
649 0 : break;
650 0 : case MFA_PHASE_RETURN_YES:
651 0 : return MHD_YES;
652 0 : case MFA_PHASE_RETURN_NO:
653 0 : GNUNET_break (0);
654 0 : return MHD_NO;
655 : }
656 : }
657 : }
|