taler-merchant-httpd_post-challenge-ID.c (20354B)
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 src/backend/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 #include "merchant-database/lookup_mfa_challenge.h" 30 #include "merchant-database/update_mfa_challenge.h" 31 32 33 /** 34 * How many attempts do we allow per solution at most? Note that 35 * this is just for the API, the value must also match the 36 * database logic in create_mfa_challenge. 37 */ 38 #define MAX_SOLUTIONS 3 39 40 41 /** 42 * How long is an OTP code valid? 43 */ 44 #define OTP_TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 30) 45 46 47 /** 48 * Internal state for MFA processing. 49 */ 50 struct MfaState 51 { 52 53 /** 54 * Kept in a DLL. 55 */ 56 struct MfaState *next; 57 58 /** 59 * Kept in a DLL. 60 */ 61 struct MfaState *prev; 62 63 /** 64 * HTTP request we are handling. 65 */ 66 struct TMH_HandlerContext *hc; 67 68 /** 69 * Challenge code. 70 */ 71 char *code; 72 73 /** 74 * When does @e code expire? 75 */ 76 struct GNUNET_TIME_Absolute expiration_date; 77 78 /** 79 * When may we transmit a new code? 80 */ 81 struct GNUNET_TIME_Absolute retransmission_date; 82 83 /** 84 * Handle to the helper process. 85 */ 86 struct GNUNET_Process *child; 87 88 /** 89 * Handle to wait for @e child 90 */ 91 struct GNUNET_ChildWaitHandle *cwh; 92 93 /** 94 * Address where to send the challenge. 95 */ 96 char *required_address; 97 98 /** 99 * Message to send. 100 */ 101 char *msg; 102 103 /** 104 * Instance the challenge is for. 105 */ 106 char *instance_id; 107 108 /** 109 * Offset of transmission in msg. 110 */ 111 size_t msg_off; 112 113 /** 114 * ID of our challenge. 115 */ 116 uint64_t challenge_id; 117 118 /** 119 * Salted hash over the request body. 120 */ 121 struct TALER_MERCHANT_MFA_BodyHash h_body; 122 123 /** 124 * Channel to use for the challenge. 125 */ 126 enum TALER_MERCHANT_MFA_Channel channel; 127 128 enum 129 { 130 MFA_PHASE_PARSE = 0, 131 MFA_PHASE_LOOKUP, 132 MFA_PHASE_SENDING, 133 MFA_PHASE_SUSPENDING, 134 MFA_PHASE_SENT, 135 MFA_PHASE_RETURN_YES, 136 MFA_PHASE_RETURN_NO, 137 138 } phase; 139 140 141 /** 142 * #GNUNET_NO if the @e connection was not suspended, 143 * #GNUNET_YES if the @e connection was suspended, 144 * #GNUNET_SYSERR if @e connection was resumed to as 145 * part of #THM_mfa_done during shutdown. 146 */ 147 enum GNUNET_GenericReturnValue suspended; 148 149 /** 150 * Type of critical operation being authorized. 151 */ 152 enum TALER_MERCHANT_MFA_CriticalOperation op; 153 154 /** 155 * Set to true if sending worked. 156 */ 157 bool send_ok; 158 }; 159 160 161 /** 162 * Kept in a DLL. 163 */ 164 static struct MfaState *mfa_head; 165 166 /** 167 * Kept in a DLL. 168 */ 169 static struct MfaState *mfa_tail; 170 171 172 /** 173 * Clean up @a mfa process. 174 * 175 * @param[in] cls the `struct MfaState` to clean up 176 */ 177 static void 178 mfa_context_cleanup (void *cls) 179 { 180 struct MfaState *mfa = cls; 181 182 GNUNET_CONTAINER_DLL_remove (mfa_head, 183 mfa_tail, 184 mfa); 185 if (NULL != mfa->cwh) 186 { 187 GNUNET_wait_child_cancel (mfa->cwh); 188 mfa->cwh = NULL; 189 } 190 if (NULL != mfa->child) 191 { 192 GNUNET_break (GNUNET_OK == 193 GNUNET_process_kill (mfa->child, 194 SIGKILL)); 195 GNUNET_break (GNUNET_OK == 196 GNUNET_process_wait (mfa->child, 197 true, 198 NULL, 199 NULL)); 200 GNUNET_process_destroy (mfa->child); 201 mfa->child = NULL; 202 } 203 GNUNET_free (mfa->required_address); 204 GNUNET_free (mfa->msg); 205 GNUNET_free (mfa->instance_id); 206 GNUNET_free (mfa->code); 207 GNUNET_free (mfa); 208 } 209 210 211 void 212 TMH_challenge_done () 213 { 214 for (struct MfaState *mfa = mfa_head; 215 NULL != mfa; 216 mfa = mfa->next) 217 { 218 if (GNUNET_YES == mfa->suspended) 219 { 220 /* Make sure transmission_done_cb() cannot fire after 221 we resumed the connection here. */ 222 if (NULL != mfa->cwh) 223 { 224 GNUNET_wait_child_cancel (mfa->cwh); 225 mfa->cwh = NULL; 226 } 227 mfa->suspended = GNUNET_SYSERR; 228 MHD_resume_connection (mfa->hc->connection); 229 } 230 } 231 } 232 233 234 /** 235 * Send the given @a response for the @a mfa request. 236 * 237 * @param[in,out] mfa process to generate an error response for 238 * @param response_code response code to use 239 * @param[in] response response data to send back 240 */ 241 static void 242 respond_to_challenge_with_response (struct MfaState *mfa, 243 unsigned int response_code, 244 struct MHD_Response *response) 245 { 246 enum MHD_Result res; 247 248 res = MHD_queue_response (mfa->hc->connection, 249 response_code, 250 response); 251 MHD_destroy_response (response); 252 mfa->phase = (MHD_NO == res) 253 ? MFA_PHASE_RETURN_NO 254 : MFA_PHASE_RETURN_YES; 255 } 256 257 258 /** 259 * Generate an error for @a mfa. 260 * 261 * @param[in,out] mfa process to generate an error response for 262 * @param http_status HTTP status of the response 263 * @param ec Taler error code to return 264 * @param hint hint to return, can be NULL 265 */ 266 static void 267 respond_with_error (struct MfaState *mfa, 268 unsigned int http_status, 269 enum TALER_ErrorCode ec, 270 const char *hint) 271 { 272 respond_to_challenge_with_response ( 273 mfa, 274 http_status, 275 TALER_MHD_make_error (ec, 276 hint)); 277 } 278 279 280 /** 281 * Challenge code transmission complete. Continue based on the result. 282 * 283 * @param[in,out] mfa process to send the challenge for 284 */ 285 static void 286 phase_sent (struct MfaState *mfa) 287 { 288 enum GNUNET_DB_QueryStatus qs; 289 290 if (! mfa->send_ok) 291 { 292 respond_with_error (mfa, 293 MHD_HTTP_BAD_GATEWAY, 294 TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED, 295 "process exited with error"); 296 return; 297 } 298 qs = TALER_MERCHANTDB_update_mfa_challenge (TMH_db, 299 mfa->challenge_id, 300 mfa->code, 301 MAX_SOLUTIONS, 302 mfa->expiration_date, 303 mfa->retransmission_date); 304 switch (qs) 305 { 306 case GNUNET_DB_STATUS_HARD_ERROR: 307 GNUNET_break (0); 308 respond_with_error (mfa, 309 MHD_HTTP_INTERNAL_SERVER_ERROR, 310 TALER_EC_GENERIC_DB_COMMIT_FAILED, 311 "update_mfa_challenge"); 312 return; 313 case GNUNET_DB_STATUS_SOFT_ERROR: 314 GNUNET_break (0); 315 respond_with_error (mfa, 316 MHD_HTTP_INTERNAL_SERVER_ERROR, 317 TALER_EC_GENERIC_DB_SOFT_FAILURE, 318 "update_mfa_challenge"); 319 return; 320 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 321 GNUNET_break (0); 322 respond_with_error (mfa, 323 MHD_HTTP_INTERNAL_SERVER_ERROR, 324 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 325 "no results on INSERT, but success?"); 326 return; 327 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 328 break; 329 } 330 { 331 struct MHD_Response *response; 332 333 response = 334 TALER_MHD_make_json_steal ( 335 GNUNET_JSON_PACK ( 336 GNUNET_JSON_pack_timestamp ( 337 "solve_expiration", 338 GNUNET_TIME_absolute_to_timestamp ( 339 mfa->expiration_date)), 340 GNUNET_JSON_pack_timestamp ( 341 "earliest_retransmission", 342 GNUNET_TIME_absolute_to_timestamp ( 343 mfa->retransmission_date)))); 344 respond_to_challenge_with_response ( 345 mfa, 346 MHD_HTTP_OK, 347 response); 348 } 349 } 350 351 352 /** 353 * Function called when our SMS helper has terminated. 354 * 355 * @param cls our `struct ANASTASIS_AUHTORIZATION_State` 356 * @param type type of the process 357 * @param exit_code status code of the process 358 */ 359 static void 360 transmission_done_cb (void *cls, 361 enum GNUNET_OS_ProcessStatusType type, 362 long unsigned int exit_code) 363 { 364 struct MfaState *mfa = cls; 365 366 mfa->cwh = NULL; 367 if (NULL != mfa->child) 368 { 369 GNUNET_process_destroy (mfa->child); 370 mfa->child = NULL; 371 } 372 mfa->send_ok = ( (GNUNET_OS_PROCESS_EXITED == type) && 373 (0 == exit_code) ); 374 if (! mfa->send_ok) 375 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 376 "MFA helper failed with status %d/%u\n", 377 (int) type, 378 (unsigned int) exit_code); 379 mfa->phase = MFA_PHASE_SENT; 380 GNUNET_assert (GNUNET_YES == mfa->suspended); 381 mfa->suspended = GNUNET_NO; 382 MHD_resume_connection (mfa->hc->connection); 383 TALER_MHD_daemon_trigger (); 384 } 385 386 387 /** 388 * Resolve a binary name via PATH. 389 * 390 * Needed because the GNUnet process helpers to not support 391 * an execp equivalent at present. 392 * 393 * @param binary_name name to search for 394 * @returns resolved path or NULL if not found 395 */ 396 static char * 397 resolve_path (const char *binary_name) 398 { 399 char *path_env; 400 char full_path[2048]; 401 char *dir; 402 char *path_copy; 403 404 if (NULL != strchr (binary_name, 405 '/')) 406 { 407 /* Already a full path, do not search. */ 408 return GNUNET_strdup (binary_name); 409 } 410 path_env = getenv ("PATH"); 411 if (path_env == NULL) 412 return NULL; 413 /* Duplicate PATH because strtok modifies the string it parses */ 414 path_copy = GNUNET_strdup (path_env); 415 dir = strtok (path_copy, ":"); 416 while (dir != NULL) 417 { 418 snprintf (full_path, 419 sizeof(full_path), 420 "%s/%s", 421 dir, 422 binary_name); 423 if (0 == access (full_path, 424 X_OK)) 425 { 426 GNUNET_free (path_copy); 427 return GNUNET_strdup (full_path); 428 } 429 dir = strtok (NULL, ":"); 430 } 431 GNUNET_free (path_copy); 432 return NULL; 433 } 434 435 436 /** 437 * Setup challenge code for @a mfa and send it to the 438 * @a required_address; on success. 439 * 440 * @param[in,out] mfa process to send the challenge for 441 */ 442 static void 443 phase_send_challenge (struct MfaState *mfa) 444 { 445 const char *prog = NULL; 446 char *binary_path = NULL; 447 unsigned long long challenge_num; 448 char **cmd_argv = NULL; 449 450 challenge_num = (unsigned long long) 451 GNUNET_CRYPTO_random_u64 (1000 * 1000 * 100); 452 GNUNET_asprintf (&mfa->code, 453 "%04llu-%04llu", 454 challenge_num / 10000, 455 challenge_num % 10000); 456 switch (mfa->channel) 457 { 458 case TALER_MERCHANT_MFA_CHANNEL_NONE: 459 GNUNET_assert (0); 460 break; 461 case TALER_MERCHANT_MFA_CHANNEL_SMS: 462 mfa->expiration_date 463 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 464 mfa->retransmission_date 465 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 466 prog = TMH_helper_sms; 467 break; 468 case TALER_MERCHANT_MFA_CHANNEL_EMAIL: 469 mfa->expiration_date 470 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 471 mfa->retransmission_date 472 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 473 prog = TMH_helper_email; 474 break; 475 case TALER_MERCHANT_MFA_CHANNEL_TOTP: 476 mfa->expiration_date 477 = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT); 478 mfa->retransmission_date 479 = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT); 480 respond_with_error (mfa, 481 MHD_HTTP_NOT_IMPLEMENTED, 482 TALER_EC_GENERIC_FEATURE_NOT_IMPLEMENTED, 483 "#10327"); 484 goto done; 485 } 486 if (NULL == prog) 487 { 488 respond_with_error ( 489 mfa, 490 MHD_HTTP_INTERNAL_SERVER_ERROR, 491 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 492 TALER_MERCHANT_MFA_channel_to_string (mfa->channel)); 493 goto done; 494 } 495 { 496 /* Start child process and feed pipe */ 497 struct GNUNET_DISK_PipeHandle *p; 498 struct GNUNET_DISK_FileHandle *pipe_stdin; 499 const char *extra_args[] = { 500 mfa->required_address, 501 NULL, 502 }; 503 504 cmd_argv = TALER_words_split (prog, 505 extra_args); 506 507 GNUNET_assert (NULL != cmd_argv[0]); 508 509 p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); 510 if (NULL == p) 511 { 512 respond_with_error (mfa, 513 MHD_HTTP_INTERNAL_SERVER_ERROR, 514 TALER_EC_GENERIC_ALLOCATION_FAILURE, 515 "pipe"); 516 goto done; 517 } 518 mfa->child = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 519 GNUNET_assert (GNUNET_OK == 520 GNUNET_process_set_options ( 521 mfa->child, 522 GNUNET_process_option_inherit_rpipe (p, 523 STDIN_FILENO))); 524 binary_path = resolve_path (cmd_argv[0]); 525 if ( (NULL == binary_path) || 526 (GNUNET_OK != 527 GNUNET_process_run_command_argv (mfa->child, 528 binary_path, 529 (const char **) cmd_argv)) ) 530 { 531 GNUNET_process_destroy (mfa->child); 532 mfa->child = NULL; 533 GNUNET_break (GNUNET_OK == 534 GNUNET_DISK_pipe_close (p)); 535 respond_with_error (mfa, 536 MHD_HTTP_BAD_GATEWAY, 537 TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED, 538 "exec"); 539 goto done; 540 } 541 542 pipe_stdin = GNUNET_DISK_pipe_detach_end (p, 543 GNUNET_DISK_PIPE_END_WRITE); 544 GNUNET_assert (NULL != pipe_stdin); 545 GNUNET_break (GNUNET_OK == 546 GNUNET_DISK_pipe_close (p)); 547 GNUNET_asprintf (&mfa->msg, 548 "%s is your security code.\n" 549 "Do not share your code with anyone.\n\n" 550 "Authorizes: %s\n" 551 "Login: %s\n\n" 552 "Expires: %s (%s).\n", 553 mfa->code, 554 TALER_MERCHANT_MFA_co2s (mfa->op), 555 mfa->instance_id, 556 GNUNET_TIME_absolute2s ( 557 mfa->expiration_date), 558 GNUNET_TIME_relative2s ( 559 GNUNET_TIME_absolute_get_remaining ( 560 mfa->expiration_date), 561 true)); 562 { 563 const char *off = mfa->msg; 564 size_t left = strlen (off); 565 566 while (0 != left) 567 { 568 ssize_t ret; 569 570 ret = GNUNET_DISK_file_write (pipe_stdin, 571 off, 572 left); 573 if (ret <= 0) 574 { 575 GNUNET_DISK_file_close (pipe_stdin); 576 respond_with_error (mfa, 577 MHD_HTTP_BAD_GATEWAY, 578 TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED, 579 "write"); 580 goto done; 581 } 582 mfa->msg_off += ret; 583 off += ret; 584 left -= ret; 585 } 586 GNUNET_DISK_file_close (pipe_stdin); 587 } 588 } 589 mfa->phase = MFA_PHASE_SUSPENDING; 590 done: 591 GNUNET_free (binary_path); 592 TALER_words_destroy (cmd_argv); 593 } 594 595 596 /** 597 * Lookup challenge in DB. 598 * 599 * @param[in,out] mfa process to parse data for 600 */ 601 static void 602 phase_lookup (struct MfaState *mfa) 603 { 604 enum GNUNET_DB_QueryStatus qs; 605 uint32_t retry_counter; 606 struct GNUNET_TIME_Absolute confirmation_date; 607 struct GNUNET_TIME_Absolute retransmission_date; 608 struct TALER_MERCHANT_MFA_BodySalt salt; 609 610 qs = TALER_MERCHANTDB_lookup_mfa_challenge (TMH_db, 611 mfa->challenge_id, 612 &mfa->h_body, 613 &salt, 614 &mfa->required_address, 615 &mfa->op, 616 &confirmation_date, 617 &retransmission_date, 618 &retry_counter, 619 &mfa->channel, 620 &mfa->instance_id); 621 switch (qs) 622 { 623 case GNUNET_DB_STATUS_HARD_ERROR: 624 GNUNET_break (0); 625 respond_with_error (mfa, 626 MHD_HTTP_INTERNAL_SERVER_ERROR, 627 TALER_EC_GENERIC_DB_COMMIT_FAILED, 628 "lookup_mfa_challenge"); 629 return; 630 case GNUNET_DB_STATUS_SOFT_ERROR: 631 GNUNET_break (0); 632 respond_with_error (mfa, 633 MHD_HTTP_INTERNAL_SERVER_ERROR, 634 TALER_EC_GENERIC_DB_SOFT_FAILURE, 635 "lookup_mfa_challenge"); 636 return; 637 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 638 GNUNET_break_op (0); 639 respond_with_error (mfa, 640 MHD_HTTP_NOT_FOUND, 641 TALER_EC_MERCHANT_TAN_CHALLENGE_UNKNOWN, 642 mfa->hc->infix); 643 return; 644 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 645 break; 646 } 647 if (! GNUNET_TIME_absolute_is_future (confirmation_date)) 648 { 649 /* was already solved */ 650 respond_with_error (mfa, 651 MHD_HTTP_GONE, 652 TALER_EC_MERCHANT_TAN_CHALLENGE_SOLVED, 653 NULL); 654 return; 655 } 656 if (GNUNET_TIME_absolute_is_future (retransmission_date)) 657 { 658 /* too early to try again */ 659 respond_with_error (mfa, 660 MHD_HTTP_TOO_MANY_REQUESTS, 661 TALER_EC_MERCHANT_TAN_TOO_EARLY, 662 GNUNET_TIME_absolute2s (retransmission_date)); 663 return; 664 } 665 mfa->phase++; 666 } 667 668 669 /** 670 * Parse challenge request. 671 * 672 * @param[in,out] mfa process to parse data for 673 */ 674 static void 675 phase_parse (struct MfaState *mfa) 676 { 677 struct TMH_HandlerContext *hc = mfa->hc; 678 enum GNUNET_GenericReturnValue ret; 679 680 ret = TMH_mfa_parse_challenge_id (hc, 681 hc->infix, 682 &mfa->challenge_id, 683 &mfa->h_body); 684 if (GNUNET_OK != ret) 685 { 686 mfa->phase = (GNUNET_NO == ret) 687 ? MFA_PHASE_RETURN_YES 688 : MFA_PHASE_RETURN_NO; 689 return; 690 } 691 mfa->phase++; 692 } 693 694 695 enum MHD_Result 696 TMH_post_challenge_ID (const struct TMH_RequestHandler *rh, 697 struct MHD_Connection *connection, 698 struct TMH_HandlerContext *hc) 699 { 700 struct MfaState *mfa = hc->ctx; 701 702 if (NULL == mfa) 703 { 704 mfa = GNUNET_new (struct MfaState); 705 mfa->hc = hc; 706 hc->ctx = mfa; 707 hc->cc = &mfa_context_cleanup; 708 GNUNET_CONTAINER_DLL_insert (mfa_head, 709 mfa_tail, 710 mfa); 711 } 712 713 while (1) 714 { 715 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 716 "Processing /challenge in phase %d\n", 717 (int) mfa->phase); 718 switch (mfa->phase) 719 { 720 case MFA_PHASE_PARSE: 721 phase_parse (mfa); 722 break; 723 case MFA_PHASE_LOOKUP: 724 phase_lookup (mfa); 725 break; 726 case MFA_PHASE_SENDING: 727 phase_send_challenge (mfa); 728 break; 729 case MFA_PHASE_SUSPENDING: 730 mfa->cwh = GNUNET_wait_child (mfa->child, 731 &transmission_done_cb, 732 mfa); 733 if (NULL == mfa->cwh) 734 { 735 respond_with_error (mfa, 736 MHD_HTTP_INTERNAL_SERVER_ERROR, 737 TALER_EC_GENERIC_ALLOCATION_FAILURE, 738 "GNUNET_wait_child"); 739 continue; 740 } 741 mfa->suspended = GNUNET_YES; 742 MHD_suspend_connection (hc->connection); 743 return MHD_YES; 744 case MFA_PHASE_SENT: 745 phase_sent (mfa); 746 break; 747 case MFA_PHASE_RETURN_YES: 748 return MHD_YES; 749 case MFA_PHASE_RETURN_NO: 750 GNUNET_break (0); 751 return MHD_NO; 752 } 753 } 754 }