taler-merchant-httpd_post-challenge-ID.c (17542B)
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 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 (void) GNUNET_OS_process_kill (mfa->child, 186 SIGKILL); 187 GNUNET_break (GNUNET_OK == 188 GNUNET_OS_process_wait (mfa->child)); 189 mfa->child = NULL; 190 } 191 GNUNET_free (mfa->required_address); 192 GNUNET_free (mfa->msg); 193 GNUNET_free (mfa->code); 194 GNUNET_free (mfa); 195 } 196 197 198 void 199 TMH_challenge_done () 200 { 201 for (struct MfaState *mfa = mfa_head; 202 NULL != mfa; 203 mfa = mfa->next) 204 { 205 if (GNUNET_YES == mfa->suspended) 206 { 207 mfa->suspended = GNUNET_SYSERR; 208 MHD_resume_connection (mfa->hc->connection); 209 } 210 } 211 } 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 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 res = MHD_queue_response (mfa->hc->connection, 229 response_code, 230 response); 231 MHD_destroy_response (response); 232 mfa->phase = (MHD_NO == res) 233 ? MFA_PHASE_RETURN_NO 234 : MFA_PHASE_RETURN_YES; 235 } 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 respond_with_error (struct MfaState *mfa, 248 unsigned int http_status, 249 enum TALER_ErrorCode ec, 250 const char *hint) 251 { 252 respond_to_challenge_with_response ( 253 mfa, 254 http_status, 255 TALER_MHD_make_error (ec, 256 hint)); 257 } 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 phase_sent (struct MfaState *mfa) 267 { 268 enum GNUNET_DB_QueryStatus qs; 269 270 if (! mfa->send_ok) 271 { 272 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 return; 277 } 278 qs = TMH_db->update_mfa_challenge (TMH_db->cls, 279 mfa->challenge_id, 280 mfa->code, 281 MAX_SOLUTIONS, 282 mfa->expiration_date, 283 mfa->retransmission_date); 284 switch (qs) 285 { 286 case GNUNET_DB_STATUS_HARD_ERROR: 287 GNUNET_break (0); 288 respond_with_error (mfa, 289 MHD_HTTP_INTERNAL_SERVER_ERROR, 290 TALER_EC_GENERIC_DB_COMMIT_FAILED, 291 "update_mfa_challenge"); 292 return; 293 case GNUNET_DB_STATUS_SOFT_ERROR: 294 GNUNET_break (0); 295 respond_with_error (mfa, 296 MHD_HTTP_INTERNAL_SERVER_ERROR, 297 TALER_EC_GENERIC_DB_SOFT_FAILURE, 298 "update_mfa_challenge"); 299 return; 300 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 301 GNUNET_break (0); 302 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 return; 307 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 308 break; 309 } 310 { 311 struct MHD_Response *response; 312 313 response = 314 TALER_MHD_make_json_steal ( 315 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 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 transmission_done_cb (void *cls, 341 enum GNUNET_OS_ProcessStatusType type, 342 long unsigned int exit_code) 343 { 344 struct MfaState *mfa = cls; 345 346 mfa->cwh = NULL; 347 if (NULL != mfa->child) 348 { 349 GNUNET_OS_process_destroy (mfa->child); 350 mfa->child = NULL; 351 } 352 mfa->send_ok = ( (GNUNET_OS_PROCESS_EXITED == type) && 353 (0 == exit_code) ); 354 if (! mfa->send_ok) 355 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 356 "MFA helper failed with status %d/%u\n", 357 (int) type, 358 (unsigned int) exit_code); 359 mfa->phase = MFA_PHASE_SENT; 360 GNUNET_assert (GNUNET_YES == mfa->suspended); 361 mfa->suspended = GNUNET_NO; 362 MHD_resume_connection (mfa->hc->connection); 363 TALER_MHD_daemon_trigger (); 364 } 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 phase_send_challenge (struct MfaState *mfa) 376 { 377 const char *prog = NULL; 378 379 switch (mfa->channel) 380 { 381 case TALER_MERCHANT_MFA_CHANNEL_NONE: 382 GNUNET_assert (0); 383 break; 384 case TALER_MERCHANT_MFA_CHANNEL_SMS: 385 mfa->expiration_date 386 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 387 mfa->retransmission_date 388 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 389 GNUNET_asprintf (&mfa->code, 390 "%llu", 391 (unsigned long long) 392 GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE, 393 100000000)); 394 prog = TMH_helper_sms; 395 break; 396 case TALER_MERCHANT_MFA_CHANNEL_EMAIL: 397 mfa->expiration_date 398 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 399 mfa->retransmission_date 400 = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); 401 GNUNET_asprintf (&mfa->code, 402 "%llu", 403 (unsigned long long) 404 GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE, 405 100000000)); 406 prog = TMH_helper_email; 407 break; 408 case TALER_MERCHANT_MFA_CHANNEL_TOTP: 409 mfa->expiration_date 410 = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT); 411 mfa->retransmission_date 412 = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT); 413 respond_with_error (mfa, 414 MHD_HTTP_NOT_IMPLEMENTED, 415 TALER_EC_GENERIC_FEATURE_NOT_IMPLEMENTED, 416 "#10327"); 417 return; 418 } 419 if (NULL == prog) 420 { 421 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 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 p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); 434 if (NULL == p) 435 { 436 respond_with_error (mfa, 437 MHD_HTTP_INTERNAL_SERVER_ERROR, 438 TALER_EC_GENERIC_ALLOCATION_FAILURE, 439 "pipe"); 440 return; 441 } 442 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 if (NULL == mfa->child) 451 { 452 GNUNET_break (GNUNET_OK == 453 GNUNET_DISK_pipe_close (p)); 454 respond_with_error (mfa, 455 MHD_HTTP_INTERNAL_SERVER_ERROR, 456 TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED, 457 "exec"); 458 return; 459 } 460 461 pipe_stdin = GNUNET_DISK_pipe_detach_end (p, 462 GNUNET_DISK_PIPE_END_WRITE); 463 GNUNET_assert (NULL != pipe_stdin); 464 GNUNET_break (GNUNET_OK == 465 GNUNET_DISK_pipe_close (p)); 466 GNUNET_asprintf (&mfa->msg, 467 "%s\nTaler-Merchant:\n%s", 468 mfa->code, 469 TALER_MERCHANT_MFA_co2s (mfa->op)); 470 { 471 const char *off = mfa->msg; 472 size_t left = strlen (off); 473 474 while (0 != left) 475 { 476 ssize_t ret; 477 478 ret = GNUNET_DISK_file_write (pipe_stdin, 479 off, 480 left); 481 if (ret <= 0) 482 { 483 respond_with_error (mfa, 484 MHD_HTTP_INTERNAL_SERVER_ERROR, 485 TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED, 486 "write"); 487 return; 488 } 489 mfa->msg_off += ret; 490 off += ret; 491 left -= ret; 492 } 493 GNUNET_DISK_file_close (pipe_stdin); 494 } 495 } 496 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 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 qs = TMH_db->lookup_mfa_challenge (TMH_db->cls, 515 mfa->challenge_id, 516 &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 switch (qs) 525 { 526 case GNUNET_DB_STATUS_HARD_ERROR: 527 GNUNET_break (0); 528 respond_with_error (mfa, 529 MHD_HTTP_INTERNAL_SERVER_ERROR, 530 TALER_EC_GENERIC_DB_COMMIT_FAILED, 531 "lookup_mfa_challenge"); 532 return; 533 case GNUNET_DB_STATUS_SOFT_ERROR: 534 GNUNET_break (0); 535 respond_with_error (mfa, 536 MHD_HTTP_INTERNAL_SERVER_ERROR, 537 TALER_EC_GENERIC_DB_SOFT_FAILURE, 538 "lookup_mfa_challenge"); 539 return; 540 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 541 GNUNET_break (0); 542 respond_with_error (mfa, 543 MHD_HTTP_NOT_FOUND, 544 TALER_EC_MERCHANT_TAN_CHALLENGE_UNKNOWN, 545 mfa->hc->infix); 546 return; 547 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 548 break; 549 } 550 if (! GNUNET_TIME_absolute_is_future (confirmation_date)) 551 { 552 /* was already solved */ 553 respond_with_error (mfa, 554 MHD_HTTP_GONE, 555 TALER_EC_MERCHANT_TAN_CHALLENGE_SOLVED, 556 NULL); 557 return; 558 } 559 if (GNUNET_TIME_absolute_is_future (retransmission_date)) 560 { 561 /* too early to try again */ 562 respond_with_error (mfa, 563 MHD_HTTP_TOO_MANY_REQUESTS, 564 TALER_EC_MERCHANT_TAN_TOO_EARLY, 565 GNUNET_TIME_absolute2s (retransmission_date)); 566 return; 567 } 568 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 phase_parse (struct MfaState *mfa) 579 { 580 struct TMH_HandlerContext *hc = mfa->hc; 581 enum GNUNET_GenericReturnValue ret; 582 583 ret = TMH_mfa_parse_challenge_id (hc, 584 hc->infix, 585 &mfa->challenge_id, 586 &mfa->h_body); 587 if (GNUNET_OK != ret) 588 { 589 mfa->phase = (GNUNET_NO == ret) 590 ? MFA_PHASE_RETURN_YES 591 : MFA_PHASE_RETURN_NO; 592 return; 593 } 594 mfa->phase++; 595 } 596 597 598 MHD_RESULT 599 TMH_post_challenge_ID (const struct TMH_RequestHandler *rh, 600 struct MHD_Connection *connection, 601 struct TMH_HandlerContext *hc) 602 { 603 struct MfaState *mfa = hc->ctx; 604 605 if (NULL == mfa) 606 { 607 mfa = GNUNET_new (struct MfaState); 608 mfa->hc = hc; 609 hc->ctx = mfa; 610 hc->cc = &mfa_context_cleanup; 611 GNUNET_CONTAINER_DLL_insert (mfa_head, 612 mfa_tail, 613 mfa); 614 } 615 616 while (1) 617 { 618 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 619 "Processing /challenge in phase %d\n", 620 (int) mfa->phase); 621 switch (mfa->phase) 622 { 623 case MFA_PHASE_PARSE: 624 phase_parse (mfa); 625 break; 626 case MFA_PHASE_LOOKUP: 627 phase_lookup (mfa); 628 break; 629 case MFA_PHASE_SENDING: 630 phase_send_challenge (mfa); 631 break; 632 case MFA_PHASE_SUSPENDING: 633 mfa->cwh = GNUNET_wait_child (mfa->child, 634 &transmission_done_cb, 635 mfa); 636 if (NULL == mfa->cwh) 637 { 638 respond_with_error (mfa, 639 MHD_HTTP_INTERNAL_SERVER_ERROR, 640 TALER_EC_GENERIC_ALLOCATION_FAILURE, 641 "GNUNET_wait_child"); 642 continue; 643 } 644 mfa->suspended = GNUNET_YES; 645 MHD_suspend_connection (hc->connection); 646 return MHD_YES; 647 case MFA_PHASE_SENT: 648 phase_sent (mfa); 649 break; 650 case MFA_PHASE_RETURN_YES: 651 return MHD_YES; 652 case MFA_PHASE_RETURN_NO: 653 GNUNET_break (0); 654 return MHD_NO; 655 } 656 } 657 }