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