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