taler-merchant-httpd_post-challenge-ID.c (18689B)
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 mfa->suspended = GNUNET_SYSERR; 221 MHD_resume_connection (mfa->hc->connection); 222 } 223 } 224 } 225 226 227 /** 228 * Send the given @a response for the @a mfa request. 229 * 230 * @param[in,out] mfa process to generate an error response for 231 * @param response_code response code to use 232 * @param[in] response response data to send back 233 */ 234 static void 235 respond_to_challenge_with_response (struct MfaState *mfa, 236 unsigned int response_code, 237 struct MHD_Response *response) 238 { 239 enum MHD_Result res; 240 241 res = MHD_queue_response (mfa->hc->connection, 242 response_code, 243 response); 244 MHD_destroy_response (response); 245 mfa->phase = (MHD_NO == res) 246 ? MFA_PHASE_RETURN_NO 247 : MFA_PHASE_RETURN_YES; 248 } 249 250 251 /** 252 * Generate an error for @a mfa. 253 * 254 * @param[in,out] mfa process to generate an error response for 255 * @param http_status HTTP status of the response 256 * @param ec Taler error code to return 257 * @param hint hint to return, can be NULL 258 */ 259 static void 260 respond_with_error (struct MfaState *mfa, 261 unsigned int http_status, 262 enum TALER_ErrorCode ec, 263 const char *hint) 264 { 265 respond_to_challenge_with_response ( 266 mfa, 267 http_status, 268 TALER_MHD_make_error (ec, 269 hint)); 270 } 271 272 273 /** 274 * Challenge code transmission complete. Continue based on the result. 275 * 276 * @param[in,out] mfa process to send the challenge for 277 */ 278 static void 279 phase_sent (struct MfaState *mfa) 280 { 281 enum GNUNET_DB_QueryStatus qs; 282 283 if (! mfa->send_ok) 284 { 285 respond_with_error (mfa, 286 MHD_HTTP_BAD_GATEWAY, 287 TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED, 288 "process exited with error"); 289 return; 290 } 291 qs = TALER_MERCHANTDB_update_mfa_challenge (TMH_db, 292 mfa->challenge_id, 293 mfa->code, 294 MAX_SOLUTIONS, 295 mfa->expiration_date, 296 mfa->retransmission_date); 297 switch (qs) 298 { 299 case GNUNET_DB_STATUS_HARD_ERROR: 300 GNUNET_break (0); 301 respond_with_error (mfa, 302 MHD_HTTP_INTERNAL_SERVER_ERROR, 303 TALER_EC_GENERIC_DB_COMMIT_FAILED, 304 "update_mfa_challenge"); 305 return; 306 case GNUNET_DB_STATUS_SOFT_ERROR: 307 GNUNET_break (0); 308 respond_with_error (mfa, 309 MHD_HTTP_INTERNAL_SERVER_ERROR, 310 TALER_EC_GENERIC_DB_SOFT_FAILURE, 311 "update_mfa_challenge"); 312 return; 313 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 314 GNUNET_break (0); 315 respond_with_error (mfa, 316 MHD_HTTP_INTERNAL_SERVER_ERROR, 317 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 318 "no results on INSERT, but success?"); 319 return; 320 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 321 break; 322 } 323 { 324 struct MHD_Response *response; 325 326 response = 327 TALER_MHD_make_json_steal ( 328 GNUNET_JSON_PACK ( 329 GNUNET_JSON_pack_timestamp ( 330 "solve_expiration", 331 GNUNET_TIME_absolute_to_timestamp ( 332 mfa->expiration_date)), 333 GNUNET_JSON_pack_timestamp ( 334 "earliest_retransmission", 335 GNUNET_TIME_absolute_to_timestamp ( 336 mfa->retransmission_date)))); 337 respond_to_challenge_with_response ( 338 mfa, 339 MHD_HTTP_OK, 340 response); 341 } 342 } 343 344 345 /** 346 * Function called when our SMS helper has terminated. 347 * 348 * @param cls our `struct ANASTASIS_AUHTORIZATION_State` 349 * @param type type of the process 350 * @param exit_code status code of the process 351 */ 352 static void 353 transmission_done_cb (void *cls, 354 enum GNUNET_OS_ProcessStatusType type, 355 long unsigned int exit_code) 356 { 357 struct MfaState *mfa = cls; 358 359 mfa->cwh = NULL; 360 if (NULL != mfa->child) 361 { 362 GNUNET_process_destroy (mfa->child); 363 mfa->child = NULL; 364 } 365 mfa->send_ok = ( (GNUNET_OS_PROCESS_EXITED == type) && 366 (0 == exit_code) ); 367 if (! mfa->send_ok) 368 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 369 "MFA helper failed with status %d/%u\n", 370 (int) type, 371 (unsigned int) exit_code); 372 mfa->phase = MFA_PHASE_SENT; 373 GNUNET_assert (GNUNET_YES == mfa->suspended); 374 mfa->suspended = GNUNET_NO; 375 MHD_resume_connection (mfa->hc->connection); 376 TALER_MHD_daemon_trigger (); 377 } 378 379 380 /** 381 * Setup challenge code for @a mfa and send it to the 382 * @a required_address; on success. 383 * 384 * @param[in,out] mfa process to send the challenge for 385 */ 386 static void 387 phase_send_challenge (struct MfaState *mfa) 388 { 389 const char *prog = NULL; 390 unsigned long long challenge_num; 391 392 challenge_num = (unsigned long long) 393 GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE, 394 1000 * 1000 * 100); 395 GNUNET_asprintf (&mfa->code, 396 "%04llu-%04llu", 397 challenge_num / 10000, 398 challenge_num % 10000); 399 switch (mfa->channel) 400 { 401 case TALER_MERCHANT_MFA_CHANNEL_NONE: 402 GNUNET_assert (0); 403 break; 404 case TALER_MERCHANT_MFA_CHANNEL_SMS: 405 mfa->expiration_date 406 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 407 mfa->retransmission_date 408 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 409 prog = TMH_helper_sms; 410 break; 411 case TALER_MERCHANT_MFA_CHANNEL_EMAIL: 412 mfa->expiration_date 413 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 414 mfa->retransmission_date 415 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 416 prog = TMH_helper_email; 417 break; 418 case TALER_MERCHANT_MFA_CHANNEL_TOTP: 419 mfa->expiration_date 420 = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT); 421 mfa->retransmission_date 422 = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT); 423 respond_with_error (mfa, 424 MHD_HTTP_NOT_IMPLEMENTED, 425 TALER_EC_GENERIC_FEATURE_NOT_IMPLEMENTED, 426 "#10327"); 427 return; 428 } 429 if (NULL == prog) 430 { 431 respond_with_error ( 432 mfa, 433 MHD_HTTP_INTERNAL_SERVER_ERROR, 434 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 435 TALER_MERCHANT_MFA_channel_to_string (mfa->channel)); 436 return; 437 } 438 { 439 /* Start child process and feed pipe */ 440 struct GNUNET_DISK_PipeHandle *p; 441 struct GNUNET_DISK_FileHandle *pipe_stdin; 442 443 p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); 444 if (NULL == p) 445 { 446 respond_with_error (mfa, 447 MHD_HTTP_INTERNAL_SERVER_ERROR, 448 TALER_EC_GENERIC_ALLOCATION_FAILURE, 449 "pipe"); 450 return; 451 } 452 mfa->child = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 453 GNUNET_assert (GNUNET_OK == 454 GNUNET_process_set_options ( 455 mfa->child, 456 GNUNET_process_option_inherit_rpipe (p, 457 STDIN_FILENO))); 458 if (GNUNET_OK != 459 GNUNET_process_run_command_va (mfa->child, 460 prog, 461 prog, 462 mfa->required_address, 463 NULL)) 464 { 465 GNUNET_process_destroy (mfa->child); 466 mfa->child = NULL; 467 GNUNET_break (GNUNET_OK == 468 GNUNET_DISK_pipe_close (p)); 469 respond_with_error (mfa, 470 MHD_HTTP_BAD_GATEWAY, 471 TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED, 472 "exec"); 473 return; 474 } 475 476 pipe_stdin = GNUNET_DISK_pipe_detach_end (p, 477 GNUNET_DISK_PIPE_END_WRITE); 478 GNUNET_assert (NULL != pipe_stdin); 479 GNUNET_break (GNUNET_OK == 480 GNUNET_DISK_pipe_close (p)); 481 GNUNET_asprintf (&mfa->msg, 482 "%s is your security code.\n" 483 "Do not share your code with anyone.\n\n" 484 "Authorizes: %s\n" 485 "Login: %s\n\n" 486 "Expires: %s (%s).\n", 487 mfa->code, 488 TALER_MERCHANT_MFA_co2s (mfa->op), 489 mfa->instance_id, 490 GNUNET_TIME_absolute2s ( 491 mfa->expiration_date), 492 GNUNET_TIME_relative2s ( 493 GNUNET_TIME_absolute_get_remaining ( 494 mfa->expiration_date), 495 true)); 496 { 497 const char *off = mfa->msg; 498 size_t left = strlen (off); 499 500 while (0 != left) 501 { 502 ssize_t ret; 503 504 ret = GNUNET_DISK_file_write (pipe_stdin, 505 off, 506 left); 507 if (ret <= 0) 508 { 509 respond_with_error (mfa, 510 MHD_HTTP_BAD_GATEWAY, 511 TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED, 512 "write"); 513 return; 514 } 515 mfa->msg_off += ret; 516 off += ret; 517 left -= ret; 518 } 519 GNUNET_DISK_file_close (pipe_stdin); 520 } 521 } 522 mfa->phase = MFA_PHASE_SUSPENDING; 523 } 524 525 526 /** 527 * Lookup challenge in DB. 528 * 529 * @param[in,out] mfa process to parse data for 530 */ 531 static void 532 phase_lookup (struct MfaState *mfa) 533 { 534 enum GNUNET_DB_QueryStatus qs; 535 uint32_t retry_counter; 536 struct GNUNET_TIME_Absolute confirmation_date; 537 struct GNUNET_TIME_Absolute retransmission_date; 538 struct TALER_MERCHANT_MFA_BodySalt salt; 539 540 qs = TALER_MERCHANTDB_lookup_mfa_challenge (TMH_db, 541 mfa->challenge_id, 542 &mfa->h_body, 543 &salt, 544 &mfa->required_address, 545 &mfa->op, 546 &confirmation_date, 547 &retransmission_date, 548 &retry_counter, 549 &mfa->channel, 550 &mfa->instance_id); 551 switch (qs) 552 { 553 case GNUNET_DB_STATUS_HARD_ERROR: 554 GNUNET_break (0); 555 respond_with_error (mfa, 556 MHD_HTTP_INTERNAL_SERVER_ERROR, 557 TALER_EC_GENERIC_DB_COMMIT_FAILED, 558 "lookup_mfa_challenge"); 559 return; 560 case GNUNET_DB_STATUS_SOFT_ERROR: 561 GNUNET_break (0); 562 respond_with_error (mfa, 563 MHD_HTTP_INTERNAL_SERVER_ERROR, 564 TALER_EC_GENERIC_DB_SOFT_FAILURE, 565 "lookup_mfa_challenge"); 566 return; 567 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 568 GNUNET_break_op (0); 569 respond_with_error (mfa, 570 MHD_HTTP_NOT_FOUND, 571 TALER_EC_MERCHANT_TAN_CHALLENGE_UNKNOWN, 572 mfa->hc->infix); 573 return; 574 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 575 break; 576 } 577 if (! GNUNET_TIME_absolute_is_future (confirmation_date)) 578 { 579 /* was already solved */ 580 respond_with_error (mfa, 581 MHD_HTTP_GONE, 582 TALER_EC_MERCHANT_TAN_CHALLENGE_SOLVED, 583 NULL); 584 return; 585 } 586 if (GNUNET_TIME_absolute_is_future (retransmission_date)) 587 { 588 /* too early to try again */ 589 respond_with_error (mfa, 590 MHD_HTTP_TOO_MANY_REQUESTS, 591 TALER_EC_MERCHANT_TAN_TOO_EARLY, 592 GNUNET_TIME_absolute2s (retransmission_date)); 593 return; 594 } 595 mfa->phase++; 596 } 597 598 599 /** 600 * Parse challenge request. 601 * 602 * @param[in,out] mfa process to parse data for 603 */ 604 static void 605 phase_parse (struct MfaState *mfa) 606 { 607 struct TMH_HandlerContext *hc = mfa->hc; 608 enum GNUNET_GenericReturnValue ret; 609 610 ret = TMH_mfa_parse_challenge_id (hc, 611 hc->infix, 612 &mfa->challenge_id, 613 &mfa->h_body); 614 if (GNUNET_OK != ret) 615 { 616 mfa->phase = (GNUNET_NO == ret) 617 ? MFA_PHASE_RETURN_YES 618 : MFA_PHASE_RETURN_NO; 619 return; 620 } 621 mfa->phase++; 622 } 623 624 625 enum MHD_Result 626 TMH_post_challenge_ID (const struct TMH_RequestHandler *rh, 627 struct MHD_Connection *connection, 628 struct TMH_HandlerContext *hc) 629 { 630 struct MfaState *mfa = hc->ctx; 631 632 if (NULL == mfa) 633 { 634 mfa = GNUNET_new (struct MfaState); 635 mfa->hc = hc; 636 hc->ctx = mfa; 637 hc->cc = &mfa_context_cleanup; 638 GNUNET_CONTAINER_DLL_insert (mfa_head, 639 mfa_tail, 640 mfa); 641 } 642 643 while (1) 644 { 645 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 646 "Processing /challenge in phase %d\n", 647 (int) mfa->phase); 648 switch (mfa->phase) 649 { 650 case MFA_PHASE_PARSE: 651 phase_parse (mfa); 652 break; 653 case MFA_PHASE_LOOKUP: 654 phase_lookup (mfa); 655 break; 656 case MFA_PHASE_SENDING: 657 phase_send_challenge (mfa); 658 break; 659 case MFA_PHASE_SUSPENDING: 660 mfa->cwh = GNUNET_wait_child (mfa->child, 661 &transmission_done_cb, 662 mfa); 663 if (NULL == mfa->cwh) 664 { 665 respond_with_error (mfa, 666 MHD_HTTP_INTERNAL_SERVER_ERROR, 667 TALER_EC_GENERIC_ALLOCATION_FAILURE, 668 "GNUNET_wait_child"); 669 continue; 670 } 671 mfa->suspended = GNUNET_YES; 672 MHD_suspend_connection (hc->connection); 673 return MHD_YES; 674 case MFA_PHASE_SENT: 675 phase_sent (mfa); 676 break; 677 case MFA_PHASE_RETURN_YES: 678 return MHD_YES; 679 case MFA_PHASE_RETURN_NO: 680 GNUNET_break (0); 681 return MHD_NO; 682 } 683 } 684 }