taler-merchant-httpd_mfa.c (23071B)
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_mfa.c 22 * @brief internal APIs for multi-factor authentication (MFA) 23 * @author Christian Grothoff 24 */ 25 #include "platform.h" 26 #include "taler-merchant-httpd.h" 27 #include "taler-merchant-httpd_mfa.h" 28 #include "merchant-database/create_mfa_challenge.h" 29 #include "merchant-database/lookup_mfa_challenge.h" 30 31 32 /** 33 * How many challenges do we allow at most per request? 34 */ 35 #define MAX_CHALLENGES 9 36 37 /** 38 * How long are challenges valid? 39 */ 40 #define CHALLENGE_LIFETIME GNUNET_TIME_UNIT_DAYS 41 42 43 enum GNUNET_GenericReturnValue 44 TMH_mfa_parse_challenge_id (struct TMH_HandlerContext *hc, 45 const char *challenge_id, 46 uint64_t *challenge_serial, 47 struct TALER_MERCHANT_MFA_BodyHash *h_body) 48 { 49 const char *dash = strchr (challenge_id, 50 '-'); 51 unsigned long long ser; 52 char min; 53 54 if (NULL == dash) 55 { 56 GNUNET_break_op (0); 57 return (MHD_NO == 58 TALER_MHD_reply_with_error (hc->connection, 59 MHD_HTTP_BAD_REQUEST, 60 TALER_EC_GENERIC_PARAMETER_MALFORMED, 61 "'-' missing in challenge ID")) 62 ? GNUNET_SYSERR 63 : GNUNET_NO; 64 } 65 if ( (2 != 66 sscanf (challenge_id, 67 "%llu%c%*s", 68 &ser, 69 &min)) || 70 ('-' != min) ) 71 { 72 GNUNET_break_op (0); 73 return (MHD_NO == 74 TALER_MHD_reply_with_error (hc->connection, 75 MHD_HTTP_BAD_REQUEST, 76 TALER_EC_GENERIC_PARAMETER_MALFORMED, 77 "Invalid number for challenge ID")) 78 ? GNUNET_SYSERR 79 : GNUNET_NO; 80 } 81 if (GNUNET_OK != 82 GNUNET_STRINGS_string_to_data (dash + 1, 83 strlen (dash + 1), 84 h_body, 85 sizeof (*h_body))) 86 { 87 GNUNET_break_op (0); 88 return (MHD_NO == 89 TALER_MHD_reply_with_error (hc->connection, 90 MHD_HTTP_BAD_REQUEST, 91 TALER_EC_GENERIC_PARAMETER_MALFORMED, 92 "Malformed challenge ID")) 93 ? GNUNET_SYSERR 94 : GNUNET_NO; 95 } 96 *challenge_serial = (uint64_t) ser; 97 return GNUNET_OK; 98 } 99 100 101 /** 102 * Check if the given authentication check was already completed. 103 * 104 * @param[in,out] hc handler context of the connection to authorize 105 * @param op operation for which we are requiring authorization 106 * @param challenge_id ID of the challenge to check if it is done 107 * @param[out] solved set to true if the challenge was solved, 108 * set to false if @a challenge_id was not found 109 * @param[out] channel TAN channel that was used, 110 * set to #TALER_MERCHANT_MFA_CHANNEL_NONE if @a challenge_id 111 * was not found 112 * @param[out] target_address address which was validated, 113 * set to NULL if @a challenge_id was not found 114 * @param[out] retry_counter how many attempts are left on the challenge 115 * @return #GNUNET_OK on success (challenge found) 116 * #GNUNET_NO if an error message was returned to the client 117 * #GNUNET_SYSERR to just close the connection 118 */ 119 static enum GNUNET_GenericReturnValue 120 mfa_challenge_check ( 121 struct TMH_HandlerContext *hc, 122 enum TALER_MERCHANT_MFA_CriticalOperation op, 123 const char *challenge_id, 124 bool *solved, 125 enum TALER_MERCHANT_MFA_Channel *channel, 126 char **target_address, 127 uint32_t *retry_counter) 128 { 129 uint64_t challenge_serial; 130 struct TALER_MERCHANT_MFA_BodyHash h_body; 131 struct TALER_MERCHANT_MFA_BodyHash x_h_body; 132 struct TALER_MERCHANT_MFA_BodySalt salt; 133 struct GNUNET_TIME_Absolute retransmission_date; 134 enum TALER_MERCHANT_MFA_CriticalOperation xop; 135 enum GNUNET_DB_QueryStatus qs; 136 struct GNUNET_TIME_Absolute confirmation_date; 137 enum GNUNET_GenericReturnValue ret; 138 char *instance_id = NULL; 139 140 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 141 "Checking status of challenge %s\n", 142 challenge_id); 143 ret = TMH_mfa_parse_challenge_id (hc, 144 challenge_id, 145 &challenge_serial, 146 &x_h_body); 147 if (GNUNET_OK != ret) 148 return ret; 149 *target_address = NULL; 150 *solved = false; 151 *channel = TALER_MERCHANT_MFA_CHANNEL_NONE; 152 *retry_counter = UINT_MAX; 153 qs = TALER_MERCHANTDB_lookup_mfa_challenge (TMH_db, 154 challenge_serial, 155 &x_h_body, 156 &salt, 157 target_address, 158 &xop, 159 &confirmation_date, 160 &retransmission_date, 161 retry_counter, 162 channel, 163 &instance_id); 164 switch (qs) 165 { 166 case GNUNET_DB_STATUS_HARD_ERROR: 167 GNUNET_break (0); 168 return (MHD_NO == 169 TALER_MHD_reply_with_error (hc->connection, 170 MHD_HTTP_INTERNAL_SERVER_ERROR, 171 TALER_EC_GENERIC_DB_COMMIT_FAILED, 172 NULL)) 173 ? GNUNET_SYSERR 174 : GNUNET_NO; 175 case GNUNET_DB_STATUS_SOFT_ERROR: 176 GNUNET_break (0); 177 return (MHD_NO == 178 TALER_MHD_reply_with_error (hc->connection, 179 MHD_HTTP_INTERNAL_SERVER_ERROR, 180 TALER_EC_GENERIC_DB_SOFT_FAILURE, 181 NULL)) 182 ? GNUNET_SYSERR 183 : GNUNET_NO; 184 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 185 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 186 "Challenge %s not found\n", 187 challenge_id); 188 return GNUNET_OK; 189 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 190 break; 191 } 192 GNUNET_free (instance_id); 193 194 if (xop != op) 195 { 196 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 197 "Challenge was for a different operation (%d!=%d)!\n", 198 (int) op, 199 (int) xop); 200 *solved = false; 201 return GNUNET_OK; 202 } 203 TALER_MERCHANT_mfa_body_hash (hc->request_body, 204 &salt, 205 &h_body); 206 if (0 != 207 GNUNET_memcmp (&h_body, 208 &x_h_body)) 209 { 210 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 211 "Challenge was for a different request body!\n"); 212 *solved = false; 213 return GNUNET_OK; 214 } 215 *solved = (! GNUNET_TIME_absolute_is_future (confirmation_date)); 216 return GNUNET_OK; 217 } 218 219 220 /** 221 * Multi-factor authentication check to see if for the given @a instance_id 222 * and the @a op operation all the TAN channels given in @a required_tans have 223 * been satisfied. Note that we always satisfy @a required_tans in the order 224 * given in the array, so if the last one is satisfied, all previous ones must 225 * have been satisfied before. 226 * 227 * If the challenges has not been satisfied, an appropriate response 228 * is returned to the client of @a hc. 229 * 230 * @param[in,out] hc handler context of the connection to authorize 231 * @param instance_name instance name to use in the message to the customer 232 * @param op operation for which we are performing 233 * @param channel TAN channel to try 234 * @param expiration_date when should the challenge expire 235 * @param required_address addresses to use for 236 * the respective challenge 237 * @param[out] challenge_id set to the challenge ID, to be freed by 238 * the caller 239 * @return #GNUNET_OK on success, 240 * #GNUNET_NO if an error message was returned to the client 241 * #GNUNET_SYSERR to just close the connection 242 */ 243 static enum GNUNET_GenericReturnValue 244 mfa_challenge_start ( 245 struct TMH_HandlerContext *hc, 246 const char *instance_name, 247 enum TALER_MERCHANT_MFA_CriticalOperation op, 248 enum TALER_MERCHANT_MFA_Channel channel, 249 struct GNUNET_TIME_Absolute expiration_date, 250 const char *required_address, 251 char **challenge_id) 252 { 253 enum GNUNET_DB_QueryStatus qs; 254 struct TALER_MERCHANT_MFA_BodySalt salt; 255 struct TALER_MERCHANT_MFA_BodyHash h_body; 256 uint64_t challenge_serial; 257 unsigned long long challenge_num; 258 char *code; 259 260 GNUNET_CRYPTO_random_block (&salt, 261 sizeof (salt)); 262 TALER_MERCHANT_mfa_body_hash (hc->request_body, 263 &salt, 264 &h_body); 265 challenge_num = (unsigned long long) 266 GNUNET_CRYPTO_random_u64 (1000 * 1000 * 100); 267 /* Note: if this is changed, the code in 268 taler-merchant-httpd_post-challenge-ID.c and 269 taler-merchant-httpd_post-challenge-ID-confirm.c must 270 possibly also be updated! */ 271 GNUNET_asprintf (&code, 272 "%04llu-%04llu", 273 challenge_num / 10000, 274 challenge_num % 10000); 275 qs = TALER_MERCHANTDB_create_mfa_challenge (TMH_db, 276 instance_name, 277 op, 278 &h_body, 279 &salt, 280 code, 281 expiration_date, 282 GNUNET_TIME_UNIT_ZERO_ABS, 283 channel, 284 required_address, 285 &challenge_serial); 286 GNUNET_free (code); 287 switch (qs) 288 { 289 case GNUNET_DB_STATUS_HARD_ERROR: 290 GNUNET_break (0); 291 return (MHD_NO == 292 TALER_MHD_reply_with_error (hc->connection, 293 MHD_HTTP_INTERNAL_SERVER_ERROR, 294 TALER_EC_GENERIC_DB_COMMIT_FAILED, 295 NULL)) 296 ? GNUNET_SYSERR 297 : GNUNET_NO; 298 case GNUNET_DB_STATUS_SOFT_ERROR: 299 GNUNET_break (0); 300 return (MHD_NO == 301 TALER_MHD_reply_with_error (hc->connection, 302 MHD_HTTP_INTERNAL_SERVER_ERROR, 303 TALER_EC_GENERIC_DB_SOFT_FAILURE, 304 NULL)) 305 ? GNUNET_SYSERR 306 : GNUNET_NO; 307 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 308 GNUNET_assert (0); 309 break; 310 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 311 break; 312 } 313 { 314 char *h_body_s; 315 316 h_body_s = GNUNET_STRINGS_data_to_string_alloc (&h_body, 317 sizeof (h_body)); 318 GNUNET_asprintf (challenge_id, 319 "%llu-%s", 320 (unsigned long long) challenge_serial, 321 h_body_s); 322 GNUNET_free (h_body_s); 323 } 324 return GNUNET_OK; 325 } 326 327 328 /** 329 * Internal book-keeping for #TMH_mfa_challenges_do(). 330 */ 331 struct Challenge 332 { 333 /** 334 * Channel on which the challenge is transmitted. 335 */ 336 enum TALER_MERCHANT_MFA_Channel channel; 337 338 /** 339 * Address to send the challenge to. 340 */ 341 const char *required_address; 342 343 /** 344 * Internal challenge ID. 345 */ 346 char *challenge_id; 347 348 /** 349 * True if the challenge was solved. 350 */ 351 bool solved; 352 353 /** 354 * True if the challenge could still be solved. 355 */ 356 bool solvable; 357 358 }; 359 360 361 /** 362 * Obtain hint about the @a target_address of type @a channel to 363 * return to the client. 364 * 365 * @param channel type of challenge 366 * @param target_address address we will sent the challenge to 367 * @return hint for the user about the address 368 */ 369 static char * 370 get_hint (enum TALER_MERCHANT_MFA_Channel channel, 371 const char *target_address) 372 { 373 switch (channel) 374 { 375 case TALER_MERCHANT_MFA_CHANNEL_NONE: 376 GNUNET_assert (0); 377 return NULL; 378 case TALER_MERCHANT_MFA_CHANNEL_SMS: 379 { 380 size_t slen = strlen (target_address); 381 const char *end; 382 383 if (slen > 4) 384 end = &target_address[slen - 4]; 385 else 386 end = &target_address[slen / 2]; 387 return GNUNET_strdup (end); 388 } 389 case TALER_MERCHANT_MFA_CHANNEL_EMAIL: 390 { 391 const char *at; 392 size_t len; 393 394 at = strchr (target_address, 395 '@'); 396 if (NULL == at) 397 len = 0; 398 else 399 len = at - target_address; 400 return GNUNET_strndup (target_address, 401 len); 402 } 403 case TALER_MERCHANT_MFA_CHANNEL_TOTP: 404 GNUNET_break (0); 405 return GNUNET_strdup ("TOTP is not implemented: #10327"); 406 } 407 GNUNET_break (0); 408 return NULL; 409 } 410 411 412 /** 413 * Check that a set of MFA challenges has been satisfied by the 414 * client for the request in @a hc. 415 * 416 * @param[in,out] hc handler context with the connection to the client 417 * @param instance_name instance name to use in the message to the customer 418 * @param op operation for which we should check challenges for 419 * @param combi_and true to tell the client to solve all challenges (AND), 420 * false means that any of the challenges will do (OR) 421 * @param ... pairs of channel and address, terminated by 422 * #TALER_MERCHANT_MFA_CHANNEL_NONE 423 * @return #GNUNET_OK on success (challenges satisfied) 424 * #GNUNET_NO if an error message was returned to the client 425 * #GNUNET_SYSERR to just close the connection 426 */ 427 enum GNUNET_GenericReturnValue 428 TMH_mfa_challenges_do ( 429 struct TMH_HandlerContext *hc, 430 const char *instance_name, 431 enum TALER_MERCHANT_MFA_CriticalOperation op, 432 bool combi_and, 433 ...) 434 { 435 struct Challenge challenges[MAX_CHALLENGES]; 436 const char *challenge_ids[MAX_CHALLENGES]; 437 size_t num_challenges; 438 char *challenge_ids_copy = NULL; 439 size_t num_provided_challenges; 440 enum GNUNET_GenericReturnValue ret; 441 442 { 443 va_list ap; 444 445 va_start (ap, 446 combi_and); 447 num_challenges = 0; 448 while (num_challenges < MAX_CHALLENGES) 449 { 450 enum TALER_MERCHANT_MFA_Channel channel; 451 const char *address; 452 453 channel = va_arg (ap, 454 enum TALER_MERCHANT_MFA_Channel); 455 if (TALER_MERCHANT_MFA_CHANNEL_NONE == channel) 456 break; 457 address = va_arg (ap, 458 const char *); 459 if (NULL == address) 460 continue; 461 challenges[num_challenges].channel = channel; 462 challenges[num_challenges].required_address = address; 463 challenges[num_challenges].challenge_id = NULL; 464 challenges[num_challenges].solved = false; 465 challenges[num_challenges].solvable = true; 466 num_challenges++; 467 } 468 va_end (ap); 469 } 470 471 if (0 == num_challenges) 472 { 473 /* No challenges required. Strange... */ 474 return GNUNET_OK; 475 } 476 477 { 478 const char *challenge_ids_header; 479 480 challenge_ids_header 481 = MHD_lookup_connection_value (hc->connection, 482 MHD_HEADER_KIND, 483 "Taler-Challenge-Ids"); 484 num_provided_challenges = 0; 485 if (NULL != challenge_ids_header) 486 { 487 challenge_ids_copy = GNUNET_strdup (challenge_ids_header); 488 489 for (char *token = strtok (challenge_ids_copy, 490 ","); 491 NULL != token; 492 token = strtok (NULL, 493 ",")) 494 { 495 if (num_provided_challenges >= MAX_CHALLENGES) 496 { 497 GNUNET_break_op (0); 498 GNUNET_free (challenge_ids_copy); 499 return (MHD_NO == 500 TALER_MHD_reply_with_error ( 501 hc->connection, 502 MHD_HTTP_BAD_REQUEST, 503 TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, 504 "Taler-Challenge-Ids")) 505 ? GNUNET_SYSERR 506 : GNUNET_NO; 507 } 508 challenge_ids[num_provided_challenges] = token; 509 num_provided_challenges++; 510 } 511 } 512 } 513 514 /* Check provided challenges against requirements */ 515 for (size_t i = 0; i < num_provided_challenges; i++) 516 { 517 bool solved; 518 enum TALER_MERCHANT_MFA_Channel channel; 519 char *target_address; 520 uint32_t retry_counter; 521 522 ret = mfa_challenge_check (hc, 523 op, 524 challenge_ids[i], 525 &solved, 526 &channel, 527 &target_address, 528 &retry_counter); 529 if (GNUNET_OK != ret) 530 goto cleanup; 531 for (size_t j = 0; j < num_challenges; j++) 532 { 533 if ( (challenges[j].channel == channel) && 534 (NULL == challenges[j].challenge_id) && 535 (NULL != target_address /* just to be sure */) && 536 (0 == strcmp (target_address, 537 challenges[j].required_address) ) ) 538 { 539 challenges[j].solved 540 = solved; 541 challenges[j].challenge_id 542 = GNUNET_strdup (challenge_ids[i]); 543 if ( (! solved) && 544 (0 == retry_counter) ) 545 { 546 /* can't be solved anymore! */ 547 challenges[i].solvable = false; 548 } 549 break; 550 } 551 } 552 GNUNET_free (target_address); 553 } 554 555 { 556 struct GNUNET_TIME_Absolute expiration_date 557 = GNUNET_TIME_relative_to_absolute (CHALLENGE_LIFETIME); 558 559 /* Start new challenges for unsolved requirements */ 560 for (size_t i = 0; i < num_challenges; i++) 561 { 562 if (NULL == challenges[i].challenge_id) 563 { 564 GNUNET_assert (! challenges[i].solved); 565 GNUNET_assert (challenges[i].solvable); 566 ret = mfa_challenge_start (hc, 567 instance_name, 568 op, 569 challenges[i].channel, 570 expiration_date, 571 challenges[i].required_address, 572 &challenges[i].challenge_id); 573 if (GNUNET_OK != ret) 574 goto cleanup; 575 } 576 } 577 } 578 579 { 580 bool all_solved = true; 581 bool any_solved = false; 582 bool solvable = true; 583 584 for (size_t i = 0; i < num_challenges; i++) 585 { 586 if (challenges[i].solved) 587 { 588 any_solved = true; 589 } 590 else 591 { 592 all_solved = false; 593 if (combi_and && 594 (! challenges[i].solvable) ) 595 solvable = false; 596 } 597 } 598 599 if ( (combi_and && all_solved) || 600 (! combi_and && any_solved) ) 601 { 602 /* Authorization successful */ 603 ret = GNUNET_OK; 604 goto cleanup; 605 } 606 if (! solvable) 607 { 608 ret = (MHD_NO == 609 TALER_MHD_reply_with_error ( 610 hc->connection, 611 MHD_HTTP_FORBIDDEN, 612 TALER_EC_MERCHANT_MFA_FORBIDDEN, 613 GNUNET_TIME_relative2s (CHALLENGE_LIFETIME, 614 false))) 615 ? GNUNET_SYSERR 616 : GNUNET_NO; 617 goto cleanup; 618 } 619 } 620 621 /* Return challenges to client */ 622 { 623 json_t *jchallenges; 624 625 jchallenges = json_array (); 626 GNUNET_assert (NULL != jchallenges); 627 for (size_t i = 0; i<num_challenges; i++) 628 { 629 const struct Challenge *c = &challenges[i]; 630 json_t *jc; 631 char *hint; 632 633 hint = get_hint (c->channel, 634 c->required_address); 635 636 jc = GNUNET_JSON_PACK ( 637 GNUNET_JSON_pack_string ("tan_info", 638 hint), 639 GNUNET_JSON_pack_string ("tan_channel", 640 TALER_MERCHANT_MFA_channel_to_string ( 641 c->channel)), 642 GNUNET_JSON_pack_string ("challenge_id", 643 c->challenge_id)); 644 GNUNET_free (hint); 645 GNUNET_assert (0 == 646 json_array_append_new ( 647 jchallenges, 648 jc)); 649 } 650 ret = (MHD_NO == 651 TALER_MHD_REPLY_JSON_PACK ( 652 hc->connection, 653 MHD_HTTP_ACCEPTED, 654 GNUNET_JSON_pack_bool ("combi_and", 655 combi_and), 656 GNUNET_JSON_pack_array_steal ("challenges", 657 jchallenges))) 658 ? GNUNET_SYSERR 659 : GNUNET_NO; 660 } 661 662 cleanup: 663 for (size_t i = 0; i < num_challenges; i++) 664 GNUNET_free (challenges[i].challenge_id); 665 GNUNET_free (challenge_ids_copy); 666 return ret; 667 } 668 669 670 enum GNUNET_GenericReturnValue 671 TMH_mfa_check_simple ( 672 struct TMH_HandlerContext *hc, 673 enum TALER_MERCHANT_MFA_CriticalOperation op, 674 struct TMH_MerchantInstance *mi) 675 { 676 enum GNUNET_GenericReturnValue ret; 677 bool have_sms = (NULL != mi->settings.phone) && 678 (NULL != TMH_helper_sms) && 679 (mi->settings.phone_validated); 680 bool have_email = (NULL != mi->settings.email) && 681 (NULL != TMH_helper_email) && 682 (mi->settings.email_validated); 683 684 /* Note: we check for 'validated' above, but in theory 685 we could also use unvalidated for this operation. 686 That's a policy-decision we may want to revise, 687 but probably need to look at the global threat model to 688 make sure alternative configurations are still sane. */ 689 if (have_email) 690 { 691 ret = TMH_mfa_challenges_do (hc, 692 mi->settings.id, 693 op, 694 false, 695 TALER_MERCHANT_MFA_CHANNEL_EMAIL, 696 mi->settings.email, 697 have_sms 698 ? TALER_MERCHANT_MFA_CHANNEL_SMS 699 : TALER_MERCHANT_MFA_CHANNEL_NONE, 700 mi->settings.phone, 701 TALER_MERCHANT_MFA_CHANNEL_NONE); 702 } 703 else if (have_sms) 704 { 705 ret = TMH_mfa_challenges_do (hc, 706 mi->settings.id, 707 op, 708 false, 709 TALER_MERCHANT_MFA_CHANNEL_SMS, 710 mi->settings.phone, 711 TALER_MERCHANT_MFA_CHANNEL_NONE); 712 } 713 else 714 { 715 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 716 "No MFA possible, skipping 2-FA\n"); 717 ret = GNUNET_OK; 718 } 719 return ret; 720 }